The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .editor
    └── .gitignore
├── .editorconfig
├── .eslintignore
├── .eslintrc.cjs
├── .github
    ├── ISSUE_TEMPLATE
    │   ├── bug_report.md
    │   └── feature_request.md
    ├── release.yml
    └── workflows
    │   └── release.yaml
├── .gitignore
├── .prettierignore
├── .prettierrc.yaml
├── .typesafe-i18n.json
├── .vale
    └── styles
    │   ├── 18F
    │       ├── Abbreviations.yml
    │       ├── Ages.yml
    │       ├── Brands.yml
    │       ├── Clarity.yml
    │       ├── Contractions.yml
    │       ├── DropDown.yml
    │       ├── Headings.yml
    │       ├── Quotes.yml
    │       ├── Reading.yml
    │       ├── SentenceLength.yml
    │       ├── Spacing.yml
    │       ├── Terms.yml
    │       ├── ToDo.yml
    │       └── UnexpandedAcronyms.yml
    │   ├── documentation
    │       ├── Abbreviations.yml
    │       ├── Ages.yml
    │       ├── CaseSensitiveTerms.yml
    │       ├── Clarity.yml
    │       ├── DropDown.yml
    │       ├── LineLength.yml
    │       ├── SentenceLength.yml
    │       ├── Spacing.yml
    │       ├── Terms.yml
    │       ├── ToDo.yml
    │       └── UnexpandedAcronyms.yml
    │   └── proselint
    │       ├── Airlinese.yml
    │       ├── AnimalLabels.yml
    │       ├── Annotations.yml
    │       ├── Apologizing.yml
    │       ├── Archaisms.yml
    │       ├── But.yml
    │       ├── Cliches.yml
    │       ├── CorporateSpeak.yml
    │       ├── Currency.yml
    │       ├── Cursing.yml
    │       ├── DateCase.yml
    │       ├── DateMidnight.yml
    │       ├── DateRedundancy.yml
    │       ├── DateSpacing.yml
    │       ├── DenizenLabels.yml
    │       ├── Diacritical.yml
    │       ├── GenderBias.yml
    │       ├── GroupTerms.yml
    │       ├── Hedging.yml
    │       ├── Hyperbole.yml
    │       ├── Jargon.yml
    │       ├── LGBTOffensive.yml
    │       ├── LGBTTerms.yml
    │       ├── Malapropisms.yml
    │       ├── Needless.yml
    │       ├── Nonwords.yml
    │       ├── Oxymorons.yml
    │       ├── P-Value.yml
    │       ├── RASSyndrome.yml
    │       ├── Skunked.yml
    │       ├── Spelling.yml
    │       ├── Typography.yml
    │       ├── Uncomparables.yml
    │       ├── Very.yml
    │       └── meta.json
├── .vscode
    ├── extensions.json
    ├── launch.json
    └── settings.json
├── .yamllint.yaml
├── CODEOWNERS
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── PRIVACY.md
├── README.md
├── SECURITY.md
├── TOS.md
├── assets
    └── open-graph.xcf
├── build
    ├── entitlements.mac.plist
    ├── icon.icns
    ├── icon.ico
    ├── icon.png
    ├── icon_1024.png
    ├── icon_128.png
    ├── icon_256.png
    ├── icon_32.png
    ├── icon_512.png
    └── icon_64.png
├── bun.lockb
├── dev-app-update.yml
├── electron-builder.yml
├── electron.vite.config.ts
├── logo.png
├── logo.svg
├── package.json
├── resources
    └── icon.png
├── scripts
    ├── build.sh
    ├── notarize.js
    ├── release.sh
    └── set-version.sh
├── src
    ├── i18n
    │   ├── de
    │   │   └── index.ts
    │   ├── en
    │   │   └── index.ts
    │   ├── formatters.ts
    │   ├── fr
    │   │   └── index.ts
    │   ├── i18n-svelte.ts
    │   ├── i18n-types.ts
    │   ├── i18n-util.async.ts
    │   ├── i18n-util.sync.ts
    │   ├── i18n-util.ts
    │   └── zh
    │   │   └── index.ts
    ├── main
    │   ├── cursors.ts
    │   ├── index.ts
    │   ├── ipcMainHandlers.ts
    │   ├── stateKeeper.ts
    │   └── utils.ts
    ├── preload
    │   ├── cursor.svg
    │   ├── cursors.ts
    │   ├── index.d.ts
    │   └── index.ts
    └── renderer
    │   ├── cursors.html
    │   ├── index.html
    │   └── src
    │       ├── About.shoulders-of-giants.json
    │       ├── About.svelte
    │       ├── App.svelte
    │       ├── AudioVisualizer.svelte
    │       ├── BananasTypes.ts
    │       ├── Config.ts
    │       ├── Host.svelte
    │       ├── Join.svelte
    │       ├── Navigation.svelte
    │       ├── Settings.svelte
    │       ├── UseSharedStore.ts
    │       ├── Utils.ts
    │       ├── WebRTC.svelte
    │       ├── cursors.css
    │       ├── env.d.ts
    │       ├── main.ts
    │       ├── overrides.css
    │       ├── stores.ts
    │       └── translations.ts
├── svelte.config.mjs
├── tsconfig.json
└── tsconfig.node.json


/.editor/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 | 


--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 | 
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true


--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | out
4 | .gitignore
5 | 


--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
 1 | module.exports = {
 2 |   parserOptions: {
 3 |     extraFileExtensions: ['.svelte']
 4 |   },
 5 |   extends: [
 6 |     'eslint:recommended',
 7 |     'plugin:svelte/recommended',
 8 |     '@electron-toolkit/eslint-config-ts/recommended',
 9 |     '@electron-toolkit/eslint-config-prettier'
10 |   ],
11 |   overrides: [
12 |     {
13 |       files: ['*.svelte'],
14 |       parser: 'svelte-eslint-parser',
15 |       parserOptions: {
16 |         parser: '@typescript-eslint/parser'
17 |       }
18 |     }
19 |   ],
20 |   rules: {
21 |     'svelte/no-unused-svelte-ignore': 'off'
22 |   }
23 | }
24 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Bug report
 3 | about: Create a report to help us improve
 4 | title: ''
 5 | labels: bug
 6 | assignees: ''
 7 | ---
 8 | 
 9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 | 
12 | **To Reproduce**
13 | Steps to reproduce the behavior:
14 | 
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 | 
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 | 
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 | 
26 | **Desktop (please complete the following information):**
27 | 
28 | - OS: [e.g. iOS]
29 | - Version [e.g. 22]
30 | 
31 | **Additional context**
32 | Add any other context about the problem here.
33 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Feature request
 3 | about: Suggest an idea for this project
 4 | title: ''
 5 | labels: enhancement
 6 | assignees: ''
 7 | ---
 8 | 
 9 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 | 
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 | 
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 | 
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 | 


--------------------------------------------------------------------------------
/.github/release.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | changelog:
 3 |   exclude:
 4 |     labels:
 5 |       - ignore-for-release
 6 |   categories:
 7 |     - title: Breaking Changes 💥
 8 |       labels:
 9 |         - breaking-change
10 |     - title: Documentation 📚
11 |       labels:
12 |         - documentation
13 |     - title: Exciting New Features ✨
14 |       labels:
15 |         - enhancement
16 |     - title: Bug Fixes 🐛
17 |       labels:
18 |         - bug
19 |     - title: Dependencies 📦
20 |       labels:
21 |         - dependencies
22 | 


--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
  1 | ---
  2 | name: Release
  3 | 
  4 | on:
  5 |   push:
  6 |     tags:
  7 |       - 'v[0-9]+.[0-9]+.[0-9]+'
  8 | jobs:
  9 |   build-linux-arm64:
 10 |     name: Build Linux ARM64
 11 |     runs-on: ubuntu-latest
 12 |     steps:
 13 |       - name: Checkout
 14 |         uses: actions/checkout@v4
 15 |         with:
 16 |           fetch-depth: 0
 17 |       - name: Set up env
 18 |         run: |
 19 |           VERSION=${GITHUB_REF_NAME#v}
 20 |           echo "VERSION=$VERSION" >> $GITHUB_ENV
 21 |           echo "PLATFORM=linux-arm64" >> $GITHUB_ENV
 22 |       - name: Install OS packages
 23 |         run: |
 24 |           sudo apt-get install -y flatpak flatpak-builder && \
 25 |           sudo snap install snapcraft --classic && \
 26 |           flatpak remote-add \
 27 |           --user \
 28 |           --if-not-exists \
 29 |           flathub https://flathub.org/repo/flathub.flatpakrepo
 30 |       - name: Set up Bun
 31 |         uses: oven-sh/setup-bun@v2
 32 |       - name: Cache Bun
 33 |         uses: actions/cache@v4
 34 |         with:
 35 |           path: ~/.bun/install/cache
 36 |           key: linux-arm64-bun-${{ hashFiles('**/bun.lockb') }}
 37 |           restore-keys: |
 38 |             linux-arm64-bun-${{ hashFiles('**/bun.lockb') }}
 39 |       - name: Cache Electron
 40 |         uses: actions/cache@v4
 41 |         with:
 42 |           path: ~/.cache/electron/
 43 |           key: linux-arm64-electron-${{ hashFiles('~/.cache/electron/**') }}
 44 |           restore-keys: |
 45 |             linux-arm64-electron-${{ hashFiles('~/.cache/electron/**') }}
 46 |       - name: Install dependencies
 47 |         run: bun install --frozen-lockfile
 48 |       - name: Build Linux ARM64
 49 |         run: ./scripts/build.sh
 50 |       - name: Linux Release
 51 |         run: ./scripts/release.sh
 52 |         env:
 53 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 54 |   build-linux:
 55 |     name: Build Linux
 56 |     runs-on: ubuntu-latest
 57 |     steps:
 58 |       - name: Checkout
 59 |         uses: actions/checkout@v4
 60 |         with:
 61 |           fetch-depth: 0
 62 |       - name: Set up env
 63 |         run: |
 64 |           VERSION=${GITHUB_REF_NAME#v}
 65 |           echo "VERSION=$VERSION" >> $GITHUB_ENV
 66 |           echo "PLATFORM=linux" >> $GITHUB_ENV
 67 |       - name: Install OS packages
 68 |         run: |
 69 |           sudo apt-get install -y flatpak flatpak-builder && \
 70 |           sudo snap install snapcraft --classic && \
 71 |           flatpak remote-add \
 72 |             --user \
 73 |             --if-not-exists \
 74 |             flathub https://flathub.org/repo/flathub.flatpakrepo
 75 |       - name: Set up Bun
 76 |         uses: oven-sh/setup-bun@v2
 77 |       - name: Cache Bun
 78 |         uses: actions/cache@v4
 79 |         with:
 80 |           path: ~/.bun/install/cache
 81 |           key: linux-bun-${{ hashFiles('**/bun.lockb') }}
 82 |           restore-keys: |
 83 |             linux-bun-${{ hashFiles('**/bun.lockb') }}
 84 |       - name: Cache Electron
 85 |         uses: actions/cache@v4
 86 |         with:
 87 |           path: ~/.cache/electron/
 88 |           key: linux-electron-${{ hashFiles('~/.cache/electron/**') }}
 89 |           restore-keys: |
 90 |             linux-electron-${{ hashFiles('~/.cache/electron/**') }}
 91 |       - name: Install dependencies
 92 |         run: bun install --frozen-lockfile
 93 |       - name: Build Linux
 94 |         run: ./scripts/build.sh
 95 |       - name: Linux Release
 96 |         run: ./scripts/release.sh
 97 |         env:
 98 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 99 |   build-windows:
100 |     name: Build Windows
101 |     runs-on: windows-latest
102 |     steps:
103 |       - name: Checkout
104 |         uses: actions/checkout@v4
105 |         with:
106 |           fetch-depth: 0
107 |       - name: Set up env
108 |         shell: bash
109 |         run: |
110 |           VERSION=${GITHUB_REF_NAME#v}
111 |           echo "VERSION=$VERSION" >> $GITHUB_ENV
112 |           echo "PLATFORM=windows" >> $GITHUB_ENV
113 |       - name: Set up Bun
114 |         uses: oven-sh/setup-bun@v2
115 |       - name: Cache Bun
116 |         uses: actions/cache@v4
117 |         with:
118 |           path: ~/.bun/install/cache
119 |           key: windows-bun-${{ hashFiles('**/bun.lockb') }}
120 |           restore-keys: |
121 |             windows-bun-${{ hashFiles('**/bun.lockb') }}
122 |       - name: Cache Electron
123 |         uses: actions/cache@v4
124 |         with:
125 |           path: ~/AppData/Local/electron/Cache/
126 |           key: windows-electron-${{ hashFiles('~/AppData/Local/electron/Cache/**') }}
127 |           restore-keys: |
128 |             windows-electron-${{ hashFiles('~/AppData/Local/electron/Cache/**') }}
129 |       - name: Install dependencies
130 |         run: bun install --frozen-lockfile
131 |       - name: Build Windows
132 |         shell: bash
133 |         run: ./scripts/build.sh
134 |       - name: Windows Release
135 |         shell: bash
136 |         run: ./scripts/release.sh
137 |         env:
138 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
139 |   build-macos:
140 |     name: Build MacOS
141 |     runs-on: macos-latest
142 |     steps:
143 |       - name: Checkout
144 |         uses: actions/checkout@v4
145 |         with:
146 |           fetch-depth: 0
147 |       - name: Install bash
148 |         run: brew install bash
149 |       - name: Set up env
150 |         run: |
151 |           VERSION=${GITHUB_REF_NAME#v}
152 |           echo "VERSION=$VERSION" >> $GITHUB_ENV
153 |           echo "PLATFORM=macos" >> $GITHUB_ENV
154 |       - name: Set up Bun
155 |         uses: oven-sh/setup-bun@v2
156 |       - name: Cache Bun
157 |         uses: actions/cache@v4
158 |         with:
159 |           path: ~/.bun/install/cache
160 |           key: macos-bun-${{ hashFiles('**/bun.lockb') }}
161 |           restore-keys: |
162 |             macos-bun-${{ hashFiles('**/bun.lockb') }}
163 |       - name: Cache Electron
164 |         uses: actions/cache@v4
165 |         with:
166 |           path: ~/Library/Caches/electron/
167 |           key: macos-electron-${{ hashFiles('~/Library/Caches/electron/**') }}
168 |           restore-keys: |
169 |             macos-electron-${{ hashFiles('~/Library/Caches/electron/**') }}
170 |       - name: Install dependencies
171 |         run: bun install --frozen-lockfile
172 |       - name: Build MacOS
173 |         env:
174 |           CSC_LINK: ${{ secrets.CSC_LINK }}
175 |           CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
176 |           APPLE_ID: ${{ secrets.APPLE_ID }}
177 |           APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
178 |           APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
179 |           APPLE_NOTARIZATION_WEBHOOK_URL: ${{ secrets.APPLE_NOTARIZATION_WEBHOOK_URL }}
180 |         run: ./scripts/build.sh
181 |       - name: MacOS Release
182 |         run: ./scripts/release.sh
183 |         env:
184 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
185 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | out
4 | .DS_Store
5 | *.log*
6 | .nvim
7 | .svelte-kit
8 | .vite
9 | 


--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | out
2 | dist
3 | pnpm-lock.yaml
4 | LICENSE.md
5 | tsconfig.json
6 | tsconfig.*.json
7 | .vale
8 | 


--------------------------------------------------------------------------------
/.prettierrc.yaml:
--------------------------------------------------------------------------------
 1 | singleQuote: true
 2 | semi: false
 3 | printWidth: 100
 4 | trailingComma: none
 5 | plugins:
 6 |   - prettier-plugin-svelte
 7 | overrides:
 8 |   - files: '*.svelte'
 9 |     options:
10 |       parser: svelte
11 | 


--------------------------------------------------------------------------------
/.typesafe-i18n.json:
--------------------------------------------------------------------------------
1 | {
2 |   "$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json",
3 |   "adapter": "svelte",
4 |   "baseLocale": "en"
5 | }
6 | 


--------------------------------------------------------------------------------
/.vale/styles/18F/Abbreviations.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | extends: substitution
 3 | message: "Use '%s' instead of '%s'."
 4 | link: https://content-guide.18f.gov/specific-words-and-phrases/
 5 | ignorecase: false
 6 | level: error
 7 | nonword: true
 8 | swap:
 9 |   '\beg\b': e.g.,
10 |   '\bie\b': i.e.,
11 |   'e\.g\.(?:[^,]|$)': e.g.,
12 |   'i\.e\.(?:[^,]|$)': i.e.,
13 |   '(?i)\d{1,2} ?[ap]m': a.m. or p.m.
14 |   'D\.C\.': DC
15 |   '\bUSA?\b': U.S.
16 | 


--------------------------------------------------------------------------------
/.vale/styles/18F/Ages.yml:
--------------------------------------------------------------------------------
1 | ---
2 | extends: existence
3 | message: Avoid hyphens in ages unless it clarifies the text.
4 | link: https://content-guide.18f.gov/specific-words-and-phrases/
5 | level: warning
6 | tokens:
7 |   - '\d{1,3}-year-old'
8 | 


--------------------------------------------------------------------------------
/.vale/styles/18F/Brands.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | extends: substitution
 3 | message: "Use '%s' instead of '%s'."
 4 | link: https://content-guide.18f.gov/trademarks-and-brands/
 5 | level: warning
 6 | ignorecase: true
 7 | swap:
 8 |   Band-Aid: bandage
 9 |   Bubble Wrap: packaging bubbles
10 |   Chapstick: lip balm
11 |   Crayola: crayons
12 |   Dumpster: waste container
13 |   Hi-Liter: highlighting marker
14 |   iPod: MP3 player
15 |   Kleenex: tissue
16 |   Plexiglas: plastic glass
17 |   Post-it note: adhesive note
18 |   Q-Tips: cotton swabs
19 |   Scotch tape: transparent tape
20 |   Styrofoam: plastic foam
21 |   Taser: stun gun
22 |   Xerox: photocopy
23 | 


--------------------------------------------------------------------------------
/.vale/styles/18F/Clarity.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | extends: substitution
 3 | message: '%s'
 4 | link: https://content-guide.18f.gov/plain-language/#words-to-avoid
 5 | level: warning
 6 | ignorecase: true
 7 | swap:
 8 |   (?:commit|pledge): >
 9 |     Be more specific — we’re either doing something or we’re not.
10 |   advancing: Avoid using 'advancing.'
11 |   agenda: Avoid using 'agenda' (unless you’re talking about a meeting).
12 |   deploy: >
13 |     Avoid using 'deploy', unless you’re talking about the military or
14 |     software.
15 |   disincentivize: Avoid using 'disincentivize.'
16 |   empower: Avoid using 'empower.'
17 |   focusing: Avoid using 'focusing.'
18 |   foster: Avoid using 'foster' (unless it’s children).
19 |   impact(?:ful)?: Avoid using impact or impactful.
20 |   incentivize: Avoid using 'incentivize.'
21 |   innovative: Use words that describe the positive outcome of the innovation.
22 |   key: Avoid using 'key' (unless it unlocks something).
23 |   leverage: >
24 |     Avoid using 'leverage' (unless you're using it in the financial sense).
25 |   progress: What are you actually doing?
26 |   promote: Avoid using 'promote' (unless you’re talking about an ad campaign).
27 |   streamline: Avoid using 'streamline.'
28 |   strengthening: >
29 |     Avoid using 'strengthening' (unless you’re referring to bridges or other
30 |     structures).
31 |   tackling: >
32 |     Avoid using 'tackling' (unless you’re referring to football or another
33 |     contact sport).
34 |   touchpoint: Mention specific system components.
35 |   transforming: What are you actually doing to change it?
36 | 


--------------------------------------------------------------------------------
/.vale/styles/18F/Contractions.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | extends: substitution
 3 | message: 'Use "%s" instead of "%s".'
 4 | link: https://content-guide.18f.gov/voice-and-tone/#use-contractions
 5 | level: error
 6 | ignorecase: true
 7 | swap:
 8 |   are not: aren't
 9 |   cannot: can't
10 |   could not: couldn't
11 |   did not: didn't
12 |   do not: don't
13 |   does not: doesn't
14 |   has not: hasn't
15 |   have not: haven't
16 |   how is: how's
17 |   how will: how'll
18 |   is not: isn't
19 |   it is: it's
20 |   it will: it'll
21 |   should not: shouldn't
22 |   that is: that's
23 |   that will: that'll
24 |   they are: they're
25 |   they will: they'll
26 |   was not: wasn't
27 |   we are: we're
28 |   we have: we've
29 |   we will: we'll
30 |   were not: weren't
31 |   what is: what's
32 |   what will: what'll
33 |   when is: when's
34 |   when will: when'll
35 |   where is: where's
36 |   where will: where'll
37 |   who is: who's
38 |   who will: who'll
39 |   why is: why's
40 |   why will: why'll
41 |   will not: won't
42 | 


--------------------------------------------------------------------------------
/.vale/styles/18F/DropDown.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | extends: substitution
 3 | message: "Use %s instead of '%s'."
 4 | link: https://content-guide.18f.gov/specific-words-and-phrases/
 5 | level: error
 6 | # 'drop-down' when used as an adjective. For example, drop-down menu.
 7 | # 'drop down' when used as a noun. For example, an option from the drop down.
 8 | # Never dropdown.
 9 | pos: 'drop/\w+ down/RP|dropdown/\w+|drop-down/NN'
10 | ignorecase: true
11 | swap:
12 |   drop down: "'drop-down'"
13 |   drop-down: "'drop down'"
14 |   dropdown: "'drop-down' or 'drop down'"
15 | 


--------------------------------------------------------------------------------
/.vale/styles/18F/Headings.yml:
--------------------------------------------------------------------------------
1 | ---
2 | extends: capitalization
3 | message: "'%s' should be in sentence case."
4 | link: https://content-guide.18f.gov/capitalization/#headings
5 | level: warning
6 | scope: heading
7 | match: $sentence
8 | 


--------------------------------------------------------------------------------
/.vale/styles/18F/Quotes.yml:
--------------------------------------------------------------------------------
1 | ---
2 | extends: existence
3 | message: Punctuation should be inside the quotes.
4 | link: https://content-guide.18f.gov/punctuation/#quotes
5 | level: error
6 | nonword: true
7 | tokens:
8 |   - '"[^"]+"[.,?]'
9 | 


--------------------------------------------------------------------------------
/.vale/styles/18F/Reading.yml:
--------------------------------------------------------------------------------
1 | ---
2 | message: Craft sentences at 25 words or fewer, whenever possible.
3 | link: 'https://content-guide.18f.gov/be-concise/#keep-sentences-short-and-sweet'
4 | extends: occurrence
5 | scope: sentence
6 | level: warning
7 | max: 25
8 | token: '\b(\w+)\b'
9 | 


--------------------------------------------------------------------------------
/.vale/styles/18F/SentenceLength.yml:
--------------------------------------------------------------------------------
1 | ---
2 | message: Craft sentences at 25 words or fewer, whenever possible.
3 | link: 'https://content-guide.18f.gov/be-concise/#keep-sentences-short-and-sweet'
4 | extends: occurrence
5 | scope: sentence
6 | level: warning
7 | max: 25
8 | token: '\b(\w+)\b'
9 | 


--------------------------------------------------------------------------------
/.vale/styles/18F/Spacing.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | extends: existence
 3 | message: "'%s' should have one space."
 4 | link: https://content-guide.18f.gov/punctuation/#spaces
 5 | level: error
 6 | nonword: true
 7 | tokens:
 8 |   - '[.?!] {2,}[A-Z]'
 9 |   - '[.?!][A-Z]'
10 | 


--------------------------------------------------------------------------------
/.vale/styles/18F/Terms.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | extends: substitution
 3 | message: "Prefer %s over '%s'."
 4 | level: warning
 5 | ignorecase: false
 6 | swap:
 7 |   'e(?:-| )mail': email
 8 |   (?:illegals|illegal aliens): "'undocumented immigrants'"
 9 |   back-end: "'back end'"
10 |   citizen: "'people' or 'The public'"
11 |   combating: "'working against' or 'fighting'"
12 |   countering: "'answering' or 'responding'"
13 |   dialogue: "'speaking(ing)'"
14 |   dropdown: "'drop-down' or 'drop down'"
15 |   execute: "'run' or 'do'"
16 |   open-source: "'open source'"
17 |   simple: "'straightforward', 'uncomplicated', or 'clear'"
18 |   user testing: "'usability testing'"
19 | 


--------------------------------------------------------------------------------
/.vale/styles/18F/ToDo.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | extends: substitution
 3 | message: "Use '%s' instead of '%s'."
 4 | link: https://content-guide.18f.gov/specific-words-and-phrases/
 5 | level: error
 6 | # 'to do' (noun) and 'to-do' (adjective). For example, your to dos or your
 7 | # to-do list.
 8 | pos: 'to/TO do/VB'
 9 | ignorecase: true
10 | swap:
11 |   to do: to-do
12 | 


--------------------------------------------------------------------------------
/.vale/styles/18F/UnexpandedAcronyms.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | extends: conditional
 3 | message: "'%s' should be parenthetically defined."
 4 | link: https://content-guide.18f.gov/abbreviations-and-acronyms/
 5 | level: warning
 6 | first: '\b([A-Z]{3,5})\b'
 7 | second: '(?:\b[A-Z][a-z]+['']? )+\(([A-Z]{3,5})\)'
 8 | exceptions:
 9 |   - ABC
10 |   - ADD
11 |   - ADHD
12 |   - AIDS
13 |   - APA
14 |   - API
15 |   - CBS
16 |   - CIA
17 |   - CSI
18 |   - CSS
19 |   - CST
20 |   - ESPN
21 |   - EST
22 |   - FAQ
23 |   - FBI
24 |   - FBI
25 |   - FIXME
26 |   - GNU
27 |   - GOV
28 |   - HIV
29 |   - HR
30 |   - HTML
31 |   - HTTP
32 |   - HTTPS
33 |   - JSON
34 |   - LAN
35 |   - MIT
36 |   - MLA
37 |   - MLB
38 |   - MTV
39 |   - NAACP
40 |   - NAACP
41 |   - NASA
42 |   - NASA
43 |   - NATO
44 |   - NBA
45 |   - NBC
46 |   - NCAA
47 |   - NCAAB
48 |   - NCAAF
49 |   - NFL
50 |   - NHL
51 |   - NOTE
52 |   - PDF
53 |   - PGA
54 |   - PPV
55 |   - PST
56 |   - SGML
57 |   - SSN
58 |   - TNT
59 |   - TODO
60 |   - URL
61 |   - USA
62 |   - USSR
63 |   - XML
64 |   - XXX
65 | 


--------------------------------------------------------------------------------
/.vale/styles/documentation/Abbreviations.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | extends: substitution
 3 | message: "Use '%s' instead of '%s'."
 4 | link: https://content-guide.18f.gov/specific-words-and-phrases/
 5 | ignorecase: false
 6 | level: error
 7 | nonword: true
 8 | swap:
 9 |   '\beg\b': e.g.,
10 |   '\bie\b': i.e.,
11 |   'e\.g\.(?:[^,]|$)': e.g.,
12 |   'i\.e\.(?:[^,]|$)': i.e.,
13 |   '(?i)\d{1,2} ?[ap]m': a.m. or p.m.
14 |   'D\.C\.': DC
15 |   '\bUSA?\b': U.S.
16 | 


--------------------------------------------------------------------------------
/.vale/styles/documentation/Ages.yml:
--------------------------------------------------------------------------------
1 | ---
2 | extends: existence
3 | message: Avoid hyphens in ages unless it clarifies the text.
4 | link: https://content-guide.18f.gov/specific-words-and-phrases/
5 | level: warning
6 | tokens:
7 |   - '\d{1,3}-year-old'
8 | 


--------------------------------------------------------------------------------
/.vale/styles/documentation/CaseSensitiveTerms.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | extends: substitution
 3 | message: "Prefer %s over '%s'."
 4 | level: warning
 5 | ignorecase: false
 6 | swap:
 7 |   devops: "'DevOps'"
 8 |   github: "'GitHub'"
 9 |   javascript: "'JavaScript'"
10 |   readme: "'README'"
11 | 


--------------------------------------------------------------------------------
/.vale/styles/documentation/Clarity.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | extends: substitution
 3 | message: '%s'
 4 | link: https://content-guide.18f.gov/plain-language/#words-to-avoid
 5 | level: warning
 6 | ignorecase: true
 7 | swap:
 8 |   pledge: >
 9 |     Be more specific than 'pledge' — we’re either doing something or we’re not.
10 |   advancing: Avoid using 'advancing.'
11 |   agenda: Avoid using 'agenda' (unless you’re talking about a meeting).
12 |   disincentivize: Avoid using 'disincentivize.'
13 |   empower: Avoid using 'empower.'
14 |   focusing: Avoid using 'focusing.'
15 |   foster: Avoid using 'foster' (unless it’s children).
16 |   impactful: Avoid using impactful.
17 |   incentivize: Avoid using 'incentivize.'
18 |   innovative: Use words that describe the positive outcome of the innovation.
19 |   leverage: >
20 |     Avoid using 'leverage' (unless you're using it in the financial sense).
21 |   promote: Avoid using 'promote' (unless you’re talking about an ad campaign).
22 |   streamline: Avoid using 'streamline.'
23 |   strengthening: >
24 |     Avoid using 'strengthening' (unless you’re referring to bridges or other
25 |     structures).
26 |   tackling: >
27 |     Avoid using 'tackling' (unless you’re referring to football or another
28 |     contact sport).
29 |   touchpoint: Mention specific system components.
30 |   transforming: What are you actually doing to change it?
31 | 


--------------------------------------------------------------------------------
/.vale/styles/documentation/DropDown.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | extends: substitution
 3 | message: "Use %s instead of '%s'."
 4 | link: https://content-guide.18f.gov/specific-words-and-phrases/
 5 | level: error
 6 | # 'drop-down' when used as an adjective. For example, drop-down menu.
 7 | # 'drop down' when used as a noun. For example, an option from the drop down.
 8 | # Never dropdown.
 9 | pos: 'drop/\w+ down/RP|dropdown/\w+|drop-down/NN'
10 | ignorecase: true
11 | swap:
12 |   drop down: "'drop-down'"
13 |   drop-down: "'drop down'"
14 |   dropdown: "'drop-down' or 'drop down'"
15 | 


--------------------------------------------------------------------------------
/.vale/styles/documentation/LineLength.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | message: A line should be at most 80 characters long.
 3 | link: https://tengolang.com/
 4 | extends: script
 5 | scope: raw
 6 | level: warning
 7 | script: |
 8 |   text := import("text")
 9 |   matches := []
10 |   // Remove all instances of code blocks since we don't want to count
11 |   // inter-block newlines as a new paragraph.
12 |   document := text.re_replace("(?s) *(\n```.*?```\n)", scope, "")
13 |   for line in text.split(document, "\n") {
14 |     // Skip links
15 |     if text.re_match("\\[(.*?)\\]: ", line) {
16 |       continue
17 |     }
18 |     if len(line) > 80 {
19 |       start := text.index(scope, line)
20 |       matches = append(matches, {begin: start, end: start + len(line)})
21 |     }
22 |   }
23 | 


--------------------------------------------------------------------------------
/.vale/styles/documentation/SentenceLength.yml:
--------------------------------------------------------------------------------
1 | ---
2 | message: Craft sentences at 25 words or fewer, whenever possible.
3 | link: 'https://content-guide.18f.gov/be-concise/#keep-sentences-short-and-sweet'
4 | extends: occurrence
5 | scope: sentence
6 | level: warning
7 | max: 25
8 | token: '\b(\w+)\b'
9 | 


--------------------------------------------------------------------------------
/.vale/styles/documentation/Spacing.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | extends: existence
 3 | message: "'%s' should have one space."
 4 | link: https://content-guide.18f.gov/punctuation/#spaces
 5 | level: error
 6 | nonword: true
 7 | tokens:
 8 |   - '[.?!] {2,}[A-Z]'
 9 |   - '[.?!][A-Z]'
10 | 


--------------------------------------------------------------------------------
/.vale/styles/documentation/Terms.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | extends: substitution
 3 | message: "Prefer %s over '%s'."
 4 | level: warning
 5 | ignorecase: true
 6 | swap:
 7 |   'e(?:-| )mail': email
 8 |   back-end: "'back end'"
 9 |   combating: "'working against' or 'fighting'"
10 |   countering: "'answering' or 'responding'"
11 |   dropdown: "'drop-down' or 'drop down'"
12 |   open-source: "'open source'"
13 |   simple: "'straightforward', 'uncomplicated', or 'clear'"
14 |   user testing: "'usability testing'"
15 | 


--------------------------------------------------------------------------------
/.vale/styles/documentation/ToDo.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | extends: substitution
 3 | message: "Use '%s' instead of '%s'."
 4 | link: https://content-guide.18f.gov/specific-words-and-phrases/
 5 | level: suggestion
 6 | # 'to do' (noun) and 'to-do' (adjective). For example, your to dos or your
 7 | # to-do list.
 8 | pos: 'to/TO do/VB'
 9 | ignorecase: true
10 | swap:
11 |   to do: to-do
12 | 


--------------------------------------------------------------------------------
/.vale/styles/documentation/UnexpandedAcronyms.yml:
--------------------------------------------------------------------------------
  1 | ---
  2 | extends: conditional
  3 | message: "'%s' should be parenthetically defined."
  4 | link: https://content-guide.18f.gov/abbreviations-and-acronyms/
  5 | level: warning
  6 | first: '\b([A-Z]{3,5})\b'
  7 | second: '(?:\b[A-Z][a-z]+['']? )+\(([A-Z]{3,5})\)'
  8 | exceptions:
  9 |   - ABI
 10 |   - ACM
 11 |   - ADC
 12 |   - ADR
 13 |   - AKA
 14 |   - APA
 15 |   - API
 16 |   - ARM
 17 |   - ASAP
 18 |   - AVR
 19 |   - CAN
 20 |   - CBS
 21 |   - CEO
 22 |   - CLI
 23 |   - CMSIS
 24 |   - CPU
 25 |   - CRC
 26 |   - CSS
 27 |   - CST
 28 |   - DMA
 29 |   - DRAM
 30 |   - DSP
 31 |   - EST
 32 |   - ETL
 33 |   - FAQ
 34 |   - FIFO
 35 |   - FILE
 36 |   - FIXME
 37 |   - GCC
 38 |   - GNU
 39 |   - GOV
 40 |   - GPIO
 41 |   - GPS
 42 |   - HAL
 43 |   - HTML
 44 |   - HTTP
 45 |   - HTTPS
 46 |   - IDE
 47 |   - IEEE
 48 |   - IRQ
 49 |   - I2C
 50 |   - JSON
 51 |   - LAN
 52 |   - LFS
 53 |   - LED
 54 |   - LLC
 55 |   - MCAPI
 56 |   - MIPI
 57 |   - MIPS
 58 |   - MIT
 59 |   - MLA
 60 |   - MLB
 61 |   - MMU
 62 |   - MRAPI
 63 |   - MSVC
 64 |   - MTAPI
 65 |   - NASA
 66 |   - NFC
 67 |   - NOT
 68 |   - NOTE
 69 |   - 'NULL'
 70 |   - OLED
 71 |   - OTA
 72 |   - PCB
 73 |   - PDF
 74 |   - PGA
 75 |   - PIC
 76 |   - POSIX
 77 |   - PPV
 78 |   - PST
 79 |   - RAII
 80 |   - RAM
 81 |   - RFID
 82 |   - ROM
 83 |   - RPC
 84 |   - RTTI
 85 |   - RTOS
 86 |   - SDK
 87 |   - SFR
 88 |   - SGML
 89 |   - SPI
 90 |   - SRAM
 91 |   - SSN
 92 |   - STL
 93 |   - TBD
 94 |   - TDD
 95 |   - TODO
 96 |   - UART
 97 |   - USART
 98 |   - UML
 99 |   - URL
100 |   - USA
101 |   - USB
102 |   - WIP
103 |   - XMI
104 |   - XML
105 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/Airlinese.yml:
--------------------------------------------------------------------------------
1 | extends: existence
2 | message: "'%s' is airlinese."
3 | ignorecase: true
4 | level: error
5 | tokens:
6 |   - enplan(?:e|ed|ing|ement)
7 |   - deplan(?:e|ed|ing|ement)
8 |   - taking off momentarily
9 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/AnimalLabels.yml:
--------------------------------------------------------------------------------
 1 | extends: substitution
 2 | message: "Consider using '%s' instead of '%s'."
 3 | level: error
 4 | action:
 5 |   name: replace
 6 | swap:
 7 |   (?:bull|ox)-like:  taurine
 8 |   (?:calf|veal)-like: vituline
 9 |   (?:crow|raven)-like: corvine
10 |   (?:leopard|panther)-like: pardine
11 |   bird-like:  avine
12 |   centipede-like:  scolopendrine
13 |   crab-like:  cancrine
14 |   crocodile-like:  crocodiline
15 |   deer-like:  damine
16 |   eagle-like:  aquiline
17 |   earthworm-like:  lumbricine
18 |   falcon-like:  falconine
19 |   ferine:  wild animal-like
20 |   fish-like:  piscine
21 |   fox-like:  vulpine
22 |   frog-like:  ranine
23 |   goat-like:  hircine
24 |   goose-like:  anserine
25 |   gull-like:  laridine
26 |   hare-like:  leporine
27 |   hawk-like:  accipitrine
28 |   hippopotamus-like:  hippopotamine
29 |   lizard-like:  lacertine
30 |   mongoose-like:  viverrine
31 |   mouse-like:  murine
32 |   ostrich-like:  struthionine
33 |   peacock-like:  pavonine
34 |   porcupine-like:  hystricine
35 |   rattlesnake-like:  crotaline
36 |   sable-like:  zibeline
37 |   sheep-like:  ovine
38 |   shrew-like:  soricine
39 |   sparrow-like:  passerine
40 |   swallow-like:  hirundine
41 |   swine-like:  suilline
42 |   tiger-like:  tigrine
43 |   viper-like:  viperine
44 |   vulture-like:  vulturine
45 |   wasp-like:  vespine
46 |   wolf-like:  lupine
47 |   woodpecker-like:  picine
48 |   zebra-like:  zebrine
49 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/Annotations.yml:
--------------------------------------------------------------------------------
 1 | extends: existence
 2 | message: "'%s' left in text."
 3 | ignorecase: false
 4 | level: error
 5 | tokens:
 6 |   - XXX
 7 |   - FIXME
 8 |   - TODO
 9 |   - NOTE
10 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/Apologizing.yml:
--------------------------------------------------------------------------------
1 | extends: existence
2 | message: "Excessive apologizing: '%s'"
3 | ignorecase: true
4 | level: error
5 | action:
6 |   name: remove
7 | tokens:
8 |   - More research is needed
9 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/Archaisms.yml:
--------------------------------------------------------------------------------
 1 | extends: existence
 2 | message: "'%s' is archaic."
 3 | ignorecase: true
 4 | level: error
 5 | tokens:
 6 |   - alack
 7 |   - anent
 8 |   - begat
 9 |   - belike
10 |   - betimes
11 |   - boughten
12 |   - brocage
13 |   - brokage
14 |   - camarade
15 |   - chiefer
16 |   - chiefest
17 |   - Christiana
18 |   - completely obsolescent
19 |   - cozen
20 |   - divers
21 |   - deflexion
22 |   - fain
23 |   - forsooth
24 |   - foreclose from
25 |   - haply
26 |   - howbeit
27 |   - illumine
28 |   - in sooth
29 |   - maugre
30 |   - meseems
31 |   - methinks
32 |   - nigh
33 |   - peradventure
34 |   - perchance
35 |   - saith
36 |   - shew
37 |   - sistren
38 |   - spake
39 |   - to wit
40 |   - verily
41 |   - whilom
42 |   - withal
43 |   - wot
44 |   - enclosed please find
45 |   - please find enclosed
46 |   - enclosed herewith
47 |   - enclosed herein
48 |   - inforce
49 |   - ex postfacto
50 |   - foreclose from
51 |   - forewent
52 |   - for ever
53 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/But.yml:
--------------------------------------------------------------------------------
1 | extends: existence
2 | message: "Do not start a paragraph with a 'but'."
3 | level: error
4 | scope: paragraph
5 | action:
6 |   name: remove
7 | tokens:
8 |   - ^But
9 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/CorporateSpeak.yml:
--------------------------------------------------------------------------------
 1 | extends: existence
 2 | message: "'%s' is corporate speak."
 3 | ignorecase: true
 4 | level: error
 5 | tokens:
 6 |   - at the end of the day
 7 |   - back to the drawing board
 8 |   - hit the ground running
 9 |   - get the ball rolling
10 |   - low-hanging fruit
11 |   - thrown under the bus
12 |   - think outside the box
13 |   - let's touch base
14 |   - get my manager's blessing
15 |   - it's on my radar
16 |   - ping me
17 |   - i don't have the bandwidth
18 |   - no brainer
19 |   - par for the course
20 |   - bang for your buck
21 |   - synergy
22 |   - move the goal post
23 |   - apples to apples
24 |   - win-win
25 |   - circle back around
26 |   - all hands on deck
27 |   - take this offline
28 |   - drill-down
29 |   - elephant in the room
30 |   - on my plate
31 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/Currency.yml:
--------------------------------------------------------------------------------
1 | extends: existence
2 | message: "Incorrect use of symbols in '%s'."
3 | ignorecase: true
4 | raw:
5 |   - \$[\d]* ?(?:dollars|usd|us dollars)
6 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/Cursing.yml:
--------------------------------------------------------------------------------
 1 | extends: existence
 2 | message: "Consider replacing '%s'."
 3 | level: error
 4 | ignorecase: true
 5 | tokens:
 6 |   - shit
 7 |   - piss
 8 |   - fuck
 9 |   - cunt
10 |   - cocksucker
11 |   - motherfucker
12 |   - tits
13 |   - fart
14 |   - turd
15 |   - twat
16 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/DateCase.yml:
--------------------------------------------------------------------------------
1 | extends: existence
2 | message: With lowercase letters, the periods are standard.
3 | ignorecase: true
4 | level: error
5 | nonword: true
6 | tokens:
7 |   - '\d{1,2} ?[ap]m'
8 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/DateMidnight.yml:
--------------------------------------------------------------------------------
1 | extends: existence
2 | message: "Use 'midnight' or 'noon'."
3 | ignorecase: true
4 | level: error
5 | nonword: true
6 | tokens:
7 |   - '12 ?[ap]\.?m\.?'
8 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/DateRedundancy.yml:
--------------------------------------------------------------------------------
 1 | extends: existence
 2 | message: "'a.m.' is always morning; 'p.m.' is always night."
 3 | ignorecase: true
 4 | level: error
 5 | nonword: true
 6 | tokens:
 7 |   - '\d{1,2} ?a\.?m\.? in the morning'
 8 |   - '\d{1,2} ?p\.?m\.? in the evening'
 9 |   - '\d{1,2} ?p\.?m\.? at night'
10 |   - '\d{1,2} ?p\.?m\.? in the afternoon'
11 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/DateSpacing.yml:
--------------------------------------------------------------------------------
1 | extends: existence
2 | message: "It's standard to put a space before '%s'"
3 | ignorecase: true
4 | level: error
5 | nonword: true
6 | tokens:
7 |   - '\d{1,2}[ap]\.?m\.?'
8 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/DenizenLabels.yml:
--------------------------------------------------------------------------------
 1 | extends: substitution
 2 | message: Did you mean '%s'?
 3 | ignorecase: false
 4 | action:
 5 |   name: replace
 6 | swap:
 7 |   (?:Afrikaaner|Afrikander):             Afrikaner
 8 |   (?:Hong Kongite|Hong Kongian):         Hong Konger
 9 |   (?:Indianan|Indianian):                Hoosier
10 |   (?:Michiganite|Michiganian):           Michigander
11 |   (?:New Hampshireite|New Hampshireman): New Hampshirite
12 |   (?:Newcastlite|Newcastleite):          Novocastrian
13 |   (?:Providencian|Providencer):          Providentian
14 |   (?:Trentian|Trentonian):               Tridentine
15 |   (?:Warsawer|Warsawian):                Varsovian
16 |   (?:Wolverhamptonite|Wolverhamptonian): Wulfrunian
17 |   Alabaman:                              Alabamian
18 |   Albuquerquian:                         Albuquerquean
19 |   Anchoragite:                           Anchorageite
20 |   Arizonian:                             Arizonan
21 |   Arkansawyer:                           Arkansan
22 |   Belarusan:                             Belarusian
23 |   Cayman Islander:                       Caymanian
24 |   Coloradoan:                            Coloradan
25 |   Connecticuter:                         Nutmegger
26 |   Fairbanksian:                          Fairbanksan
27 |   Fort Worther:                          Fort Worthian
28 |   Grenadian:                             Grenadan
29 |   Halifaxer:                             Haligonian
30 |   Hartlepoolian:                         Hartlepudlian
31 |   Illinoisian:                           Illinoisan
32 |   Iowegian:                              Iowan
33 |   Leedsian:                              Leodenisian
34 |   Liverpoolian:                          Liverpudlian
35 |   Los Angelean:                          Angeleno
36 |   Manchesterian:                         Mancunian
37 |   Minneapolisian:                        Minneapolitan
38 |   Missouran:                             Missourian
39 |   Monacan:                               Monegasque
40 |   Neopolitan:                            Neapolitan
41 |   New Jerseyite:                         New Jerseyan
42 |   New Orleansian:                        New Orleanian
43 |   Oklahoma Citian:                       Oklahoma Cityan
44 |   Oklahomian:                            Oklahoman
45 |   Saudi Arabian:                         Saudi
46 |   Seattlite:                             Seattleite
47 |   Surinamer:                             Surinamese
48 |   Tallahassean:                          Tallahasseean
49 |   Tennesseean:                           Tennessean
50 |   Trois-Rivièrester:                     Trifluvian
51 |   Utahan:                                Utahn
52 |   Valladolidian:                         Vallisoletano
53 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/Diacritical.yml:
--------------------------------------------------------------------------------
 1 | extends: substitution
 2 | message: Consider using '%s' instead of '%s'.
 3 | ignorecase: true
 4 | level: error
 5 | action:
 6 |   name: replace
 7 | swap:
 8 |   beau ideal: beau idéal
 9 |   boutonniere: boutonnière
10 |   bric-a-brac: bric-à-brac
11 |   cafe: café
12 |   cause celebre: cause célèbre
13 |   chevre: chèvre
14 |   cliche: cliché
15 |   consomme: consommé
16 |   coup de grace: coup de grâce
17 |   crudites: crudités
18 |   creme brulee: crème brûlée
19 |   creme de menthe: crème de menthe
20 |   creme fraice: crème fraîche
21 |   creme fresh: crème fraîche
22 |   crepe: crêpe
23 |   debutante: débutante
24 |   decor: décor
25 |   deja vu: déjà vu
26 |   denouement: dénouement
27 |   facade: façade
28 |   fiance: fiancé
29 |   fiancee: fiancée
30 |   flambe: flambé
31 |   garcon: garçon
32 |   lycee: lycée
33 |   maitre d: maître d
34 |   menage a trois: ménage à trois
35 |   negligee: négligée
36 |   protege: protégé
37 |   protegee: protégée
38 |   puree: purée
39 |   my resume: my résumé
40 |   your resume: your résumé
41 |   his resume: his résumé
42 |   her resume: her résumé
43 |   a resume: a résumé
44 |   the resume: the résumé
45 |   risque: risqué
46 |   roue: roué
47 |   soiree: soirée
48 |   souffle: soufflé
49 |   soupcon: soupçon
50 |   touche: touché
51 |   tete-a-tete: tête-à-tête
52 |   voila: voilà
53 |   a la carte: à la carte
54 |   a la mode: à la mode
55 |   emigre: émigré
56 | 
57 |   # Spanish loanwords
58 |   El Nino: El Niño
59 |   jalapeno: jalapeño
60 |   La Nina: La Niña
61 |   pina colada: piña colada
62 |   senor: señor
63 |   senora: señora
64 |   senorita: señorita
65 | 
66 |   # Portuguese loanwords
67 |   acai: açaí
68 | 
69 |   # German loanwords
70 |   doppelganger: doppelgänger
71 |   Fuhrer: Führer
72 |   Gewurztraminer: Gewürztraminer
73 |   vis-a-vis: vis-à-vis
74 |   Ubermensch: Übermensch
75 | 
76 |   # Swedish loanwords
77 |   filmjolk: filmjölk
78 |   smorgasbord: smörgåsbord
79 | 
80 |   # Names, places, and companies
81 |   Beyonce: Beyoncé
82 |   Bronte: Brontë
83 |   Bronte: Brontë
84 |   Champs-Elysees: Champs-Élysées
85 |   Citroen: Citroën
86 |   Curacao: Curaçao
87 |   Lowenbrau: Löwenbräu
88 |   Monegasque: Monégasque
89 |   Motley Crue: Mötley Crüe
90 |   Nescafe: Nescafé
91 |   Queensryche: Queensrÿche
92 |   Quebec: Québec
93 |   Quebecois: Québécois
94 |   Angstrom: Ångström
95 |   angstrom: ångström
96 |   Skoda: Škoda
97 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/GenderBias.yml:
--------------------------------------------------------------------------------
 1 | extends: substitution
 2 | message: Consider using '%s' instead of '%s'.
 3 | ignorecase: true
 4 | level: error
 5 | action:
 6 |   name: replace
 7 | swap:
 8 |   (?:alumnae|alumni):          graduates
 9 |   (?:alumna|alumnus):          graduate
10 |   air(?:m[ae]n|wom[ae]n):      pilot(s)
11 |   anchor(?:m[ae]n|wom[ae]n):   anchor(s)
12 |   authoress:                   author
13 |   camera(?:m[ae]n|wom[ae]n):   camera operator(s)
14 |   chair(?:m[ae]n|wom[ae]n):    chair(s)
15 |   congress(?:m[ae]n|wom[ae]n): member(s) of congress
16 |   door(?:m[ae]|wom[ae]n):      concierge(s)
17 |   draft(?:m[ae]n|wom[ae]n):    drafter(s)
18 |   fire(?:m[ae]n|wom[ae]n):     firefighter(s)
19 |   fisher(?:m[ae]n|wom[ae]n):   fisher(s)
20 |   fresh(?:m[ae]n|wom[ae]n):    first-year student(s)
21 |   garbage(?:m[ae]n|wom[ae]n):  waste collector(s)
22 |   lady lawyer:                 lawyer
23 |   ladylike:                    courteous
24 |   landlord:                    building manager
25 |   mail(?:m[ae]n|wom[ae]n):     mail carriers
26 |   man and wife:                husband and wife
27 |   man enough:                  strong enough
28 |   mankind:                     human kind
29 |   manmade:                     manufactured
30 |   men and girls:               men and women
31 |   middle(?:m[ae]n|wom[ae]n):   intermediary
32 |   news(?:m[ae]n|wom[ae]n):     journalist(s)
33 |   ombuds(?:man|woman):         ombuds
34 |   oneupmanship:                upstaging
35 |   poetess:                     poet
36 |   police(?:m[ae]n|wom[ae]n):   police officer(s)
37 |   repair(?:m[ae]n|wom[ae]n):   technician(s)
38 |   sales(?:m[ae]n|wom[ae]n):    salesperson or sales people
39 |   service(?:m[ae]n|wom[ae]n):  soldier(s)
40 |   steward(?:ess)?:             flight attendant
41 |   tribes(?:m[ae]n|wom[ae]n):   tribe member(s)
42 |   waitress:                    waiter
43 |   woman doctor:                doctor
44 |   woman scientist[s]?:         scientist(s)
45 |   work(?:m[ae]n|wom[ae]n):     worker(s)
46 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/GroupTerms.yml:
--------------------------------------------------------------------------------
 1 | extends: substitution
 2 | message: Consider using '%s' instead of '%s'.
 3 | ignorecase: true
 4 | action:
 5 |   name: replace
 6 | swap:
 7 |   (?:bunch|group|pack|flock) of chickens:    brood of chickens
 8 |   (?:bunch|group|pack|flock) of crows:       murder of crows
 9 |   (?:bunch|group|pack|flock) of hawks:       cast of hawks
10 |   (?:bunch|group|pack|flock) of parrots:     pandemonium of parrots
11 |   (?:bunch|group|pack|flock) of peacocks:    muster of peacocks
12 |   (?:bunch|group|pack|flock) of penguins:    muster of penguins
13 |   (?:bunch|group|pack|flock) of sparrows:    host of sparrows
14 |   (?:bunch|group|pack|flock) of turkeys:     rafter of turkeys
15 |   (?:bunch|group|pack|flock) of woodpeckers: descent of woodpeckers
16 |   (?:bunch|group|pack|herd) of apes:         shrewdness of apes
17 |   (?:bunch|group|pack|herd) of baboons:      troop of baboons
18 |   (?:bunch|group|pack|herd) of badgers:      cete of badgers
19 |   (?:bunch|group|pack|herd) of bears:        sloth of bears
20 |   (?:bunch|group|pack|herd) of bullfinches:  bellowing of bullfinches
21 |   (?:bunch|group|pack|herd) of bullocks:     drove of bullocks
22 |   (?:bunch|group|pack|herd) of caterpillars: army of caterpillars
23 |   (?:bunch|group|pack|herd) of cats:         clowder of cats
24 |   (?:bunch|group|pack|herd) of colts:        rag of colts
25 |   (?:bunch|group|pack|herd) of crocodiles:   bask of crocodiles
26 |   (?:bunch|group|pack|herd) of dolphins:     school of dolphins
27 |   (?:bunch|group|pack|herd) of foxes:        skulk of foxes
28 |   (?:bunch|group|pack|herd) of gorillas:     band of gorillas
29 |   (?:bunch|group|pack|herd) of hippopotami:  bloat of hippopotami
30 |   (?:bunch|group|pack|herd) of horses:       drove of horses
31 |   (?:bunch|group|pack|herd) of jellyfish:    fluther of jellyfish
32 |   (?:bunch|group|pack|herd) of kangeroos:    mob of kangeroos
33 |   (?:bunch|group|pack|herd) of monkeys:      troop of monkeys
34 |   (?:bunch|group|pack|herd) of oxen:         yoke of oxen
35 |   (?:bunch|group|pack|herd) of rhinoceros:   crash of rhinoceros
36 |   (?:bunch|group|pack|herd) of wild boar:    sounder of wild boar
37 |   (?:bunch|group|pack|herd) of wild pigs:    drift of wild pigs
38 |   (?:bunch|group|pack|herd) of zebras:       zeal of wild pigs
39 |   (?:bunch|group|pack|school) of trout:      hover of trout
40 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/Hedging.yml:
--------------------------------------------------------------------------------
1 | extends: existence
2 | message: "'%s' is hedging."
3 | ignorecase: true
4 | level: error
5 | tokens:
6 |   - I would argue that
7 |   - ', so to speak'
8 |   - to a certain degree
9 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/Hyperbole.yml:
--------------------------------------------------------------------------------
1 | extends: existence
2 | message: "'%s' is hyperbolic."
3 | level: error
4 | nonword: true
5 | tokens:
6 |   - '[a-z]+[!?]{2,}'
7 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/Jargon.yml:
--------------------------------------------------------------------------------
 1 | extends: existence
 2 | message: "'%s' is jargon."
 3 | ignorecase: true
 4 | level: error
 5 | tokens:
 6 |   - in the affirmative
 7 |   - in the negative
 8 |   - agendize
 9 |   - per your order
10 |   - per your request
11 |   - disincentivize
12 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/LGBTOffensive.yml:
--------------------------------------------------------------------------------
 1 | extends: existence
 2 | message: "'%s' is offensive. Remove it or consider the context."
 3 | ignorecase: true
 4 | tokens:
 5 |   - fag
 6 |   - faggot
 7 |   - dyke
 8 |   - sodomite
 9 |   - homosexual agenda
10 |   - gay agenda
11 |   - transvestite
12 |   - homosexual lifestyle
13 |   - gay lifestyle
14 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/LGBTTerms.yml:
--------------------------------------------------------------------------------
 1 | extends: substitution
 2 | message: "Consider using '%s' instead of '%s'."
 3 | ignorecase: true
 4 | action:
 5 |   name: replace
 6 | swap:
 7 |   homosexual man:                            gay man
 8 |   homosexual men:                            gay men
 9 |   homosexual woman:                          lesbian
10 |   homosexual women:                          lesbians
11 |   homosexual people:                         gay people
12 |   homosexual couple:                         gay couple
13 |   sexual preference:                         sexual orientation
14 |   (?:admitted homosexual|avowed homosexual): openly gay
15 |   special rights:                            equal rights
16 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/Malapropisms.yml:
--------------------------------------------------------------------------------
1 | extends: existence
2 | message: "'%s' is a malapropism."
3 | ignorecase: true
4 | level: error
5 | tokens:
6 |   - the infinitesimal universe
7 |   - a serial experience
8 |   - attack my voracity
9 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/Needless.yml:
--------------------------------------------------------------------------------
  1 | extends: substitution
  2 | message: Prefer '%s' over '%s'
  3 | ignorecase: true
  4 | action:
  5 |   name: replace
  6 | swap:
  7 |   '(?:cell phone|cell-phone)': cellphone
  8 |   '(?:cliquey|cliquy)': cliquish
  9 |   '(?:pygmean|pygmaen)': pygmy
 10 |   '(?:retributional|retributionary)': retributive
 11 |   '(?:revokable|revokeable)': revocable
 12 |   abolishment: abolition
 13 |   accessary: accessory
 14 |   accreditate: accredit
 15 |   accruement: accrual
 16 |   accusee: accused
 17 |   acquaintanceship: acquaintance
 18 |   acquitment: acquittal
 19 |   administrate: administer
 20 |   administrated: administered
 21 |   administrating: administering
 22 |   adulterate: adulterous
 23 |   advisatory: advisory
 24 |   advocator: advocate
 25 |   aggrievance: grievance
 26 |   allegator: alleger
 27 |   allusory: allusive
 28 |   amative: amorous
 29 |   amortizement: amortization
 30 |   amphiboly: amphibology
 31 |   anecdotalist: anecdotist
 32 |   anilinctus: anilingus
 33 |   anticipative: anticipatory
 34 |   antithetic: antithetical
 35 |   applicative: applicable
 36 |   applicatory: applicable
 37 |   applier: applicator
 38 |   approbative: approbatory
 39 |   arbitrager: arbitrageur
 40 |   arsenous: arsenious
 41 |   ascendance: ascendancy
 42 |   ascendence: ascendancy
 43 |   ascendency: ascendancy
 44 |   auctorial: authorial
 45 |   averral: averment
 46 |   barbwire: barbed wire
 47 |   benefic: beneficent
 48 |   benignant: benign
 49 |   bestowment: bestowal
 50 |   betrothment: betrothal
 51 |   blamableness: blameworthiness
 52 |   butt naked: buck naked
 53 |   camarade: comrade
 54 |   carta blanca: carte blanche
 55 |   casualities: casualties
 56 |   casuality: casualty
 57 |   catch on fire: catch fire
 58 |   catholicly: catholically
 59 |   cease fire: ceasefire
 60 |   channelize: channel
 61 |   chaplainship: chaplaincy
 62 |   chrysalid: chrysalis
 63 |   chrysalids: chrysalises
 64 |   cigaret: cigarette
 65 |   coemployee: coworker
 66 |   cognitional: cognitive
 67 |   cohabitate: cohabit
 68 |   cohabitor: cohabitant
 69 |   collodium: collodion
 70 |   collusory: collusive
 71 |   commemoratory: commemorative
 72 |   commonty: commonage
 73 |   communicatory: communicative
 74 |   compensative: compensatory
 75 |   complacence: complacency
 76 |   complicitous: complicit
 77 |   computate: compute
 78 |   conciliative: conciliatory
 79 |   concomitancy: concomitance
 80 |   condonance: condonation
 81 |   confirmative: confirmatory
 82 |   congruency: congruence
 83 |   connotate: connote
 84 |   consanguineal: consanguine
 85 |   conspicuity: conspicuousness
 86 |   conspiratorialist: conspirator
 87 |   constitutionist: constitutionalist
 88 |   contingence: contigency
 89 |   contributary: contributory
 90 |   contumacity: contumacy
 91 |   conversible: convertible
 92 |   conveyal: conveyance
 93 |   copartner: partner
 94 |   copartnership: partnership
 95 |   corroboratory: corroborative
 96 |   cotemporaneous: contemporaneous
 97 |   cotemporary: contemporary
 98 |   criminate: incriminate
 99 |   culpatory: inculpatory
100 |   cumbrance: encumbrance
101 |   cumulate: accumulate
102 |   curatory: curative
103 |   daredeviltry: daredevilry
104 |   deceptious: deceptive
105 |   defamative: defamatory
106 |   defraudulent: fraudulent
107 |   degeneratory: degenerative
108 |   delimitate: delimit
109 |   delusory: delusive
110 |   denouncement: denunciation
111 |   depositee: depositary
112 |   depreciative: depreciatory
113 |   deprival: deprivation
114 |   derogative: derogatory
115 |   destroyable: destructible
116 |   detoxicate: detoxify
117 |   detractory: detractive
118 |   deviancy: deviance
119 |   deviationist: deviant
120 |   digamy: deuterogamy
121 |   digitalize: digitize
122 |   diminishment: diminution
123 |   diplomatist: diplomat
124 |   disassociate: dissociate
125 |   disciplinatory: disciplinary
126 |   discriminant: discriminating
127 |   disenthrone: dethrone
128 |   disintegratory: disintegrative
129 |   dismission: dismissal
130 |   disorientate: disorient
131 |   disorientated: disoriented
132 |   disquieten: disquiet
133 |   distraite: distrait
134 |   divergency: divergence
135 |   dividable: divisible
136 |   doctrinary: doctrinaire
137 |   documental: documentary
138 |   domesticize: domesticate
139 |   duplicatory: duplicative
140 |   duteous: dutiful
141 |   educationalist: educationist
142 |   educatory: educative
143 |   enigmatas: enigmas
144 |   enlargen: enlarge
145 |   enswathe: swathe
146 |   epical: epic
147 |   erotism: eroticism
148 |   ethician: ethicist
149 |   ex officiis: ex officio
150 |   exculpative: exculpatory
151 |   exigeant: exigent
152 |   exigence: exigency
153 |   exotism: exoticism
154 |   expedience: expediency
155 |   expediential: expedient
156 |   expediential: expedient
157 |   extensible: extendable
158 |   eying: eyeing
159 |   fiefdom: fief
160 |   flagrance: flagrancy
161 |   flatulency: flatulence
162 |   fraudful: fraudulent
163 |   funebrial: funereal
164 |   geographical: geographic
165 |   geometrical: geometric
166 |   gerry-rigged: jury-rigged
167 |   goatherder: goatherd
168 |   gustatorial: gustatory
169 |   habitude: habit
170 |   henceforward: henceforth
171 |   hesitance: hesitancy
172 |   heterogenous: heterogeneous
173 |   hierarchic: hierarchical
174 |   hindermost: hindmost
175 |   honorand: honoree
176 |   hypostasize: hypostatize
177 |   hysteric: hysterical
178 |   idolatrize: idolize
179 |   impanel: empanel
180 |   imperviable: impervious
181 |   importunacy: importunity
182 |   impotency: impotence
183 |   imprimatura: imprimatur
184 |   improprietous: improper
185 |   inalterable: unalterable
186 |   incitation: incitement
187 |   incommunicative: uncommunicative
188 |   inconsistence: inconsistency
189 |   incontrollable: uncontrollable
190 |   incurment: incurrence
191 |   indow: endow
192 |   indue: endue
193 |   inhibitive: inhibitory
194 |   innavigable: unnavigable
195 |   innovational: innovative
196 |   inquisitional: inquisitorial
197 |   insistment: insistence
198 |   insolvable: unsolvable
199 |   instillment: instillation
200 |   instinctual: instinctive
201 |   insuror: insurer
202 |   insurrectional: insurrectionary
203 |   interpretate: interpret
204 |   intervenience: intervention
205 |   ironical: ironic
206 |   jerry-rigged: jury-rigged
207 |   judgmatic: judgmental
208 |   labyrinthian: labyrinthine
209 |   laudative: laudatory
210 |   legitimatization: legitimation
211 |   legitimatize: legitimize
212 |   legitimization: legitimation
213 |   lengthways: lengthwise
214 |   life-sized: life-size
215 |   liquorice: licorice
216 |   lithesome: lithe
217 |   lollipop: lollypop
218 |   loth: loath
219 |   lubricous: lubricious
220 |   maihem: mayhem
221 |   medicinal marijuana: medical marijuana
222 |   meliorate: ameliorate
223 |   minimalize: minimize
224 |   mirk: murk
225 |   mirky: murky
226 |   misdoubt: doubt
227 |   monetarize: monetize
228 |   moveable: movable
229 |   narcism: narcissism
230 |   neglective: neglectful
231 |   negligency: negligence
232 |   neologizer: neologist
233 |   neurologic: neurological
234 |   nicknack: knickknack
235 |   nictate: nictitate
236 |   nonenforceable: unenforceable
237 |   normalcy: normality
238 |   numbedness: numbness
239 |   omittable: omissible
240 |   onomatopoetic: onomatopoeic
241 |   opinioned: opined
242 |   optimum advantage: optimal advantage
243 |   orientate: orient
244 |   outsized: outsize
245 |   oversized: oversize
246 |   overthrowal: overthrow
247 |   pacificist: pacifist
248 |   paederast: pederast
249 |   parachronism: anachronism
250 |   parti-color: parti-colored
251 |   participative: participatory
252 |   party-colored: parti-colored
253 |   pediatrist: pediatrician
254 |   penumbrous: penumbral
255 |   perjorative: pejorative
256 |   permissory: permissive
257 |   permutate: permute
258 |   personation: impersonation
259 |   pharmaceutic: pharmaceutical
260 |   pleuritis: pleurisy
261 |   policy holder: policyholder
262 |   policyowner: policyholder
263 |   politicalize: politicize
264 |   precedency: precedence
265 |   preceptoral: preceptorial
266 |   precipitance: precipitancy
267 |   precipitant: precipitate
268 |   preclusory: preclusive
269 |   precolumbian: pre-Columbian
270 |   prefectoral: prefectorial
271 |   preponderately: preponderantly
272 |   preserval: preservation
273 |   preventative: preventive
274 |   proconsulship: proconsulate
275 |   procreational: procreative
276 |   procurance: procurement
277 |   propelment: propulsion
278 |   propulsory: propulsive
279 |   prosecutive: prosecutory
280 |   protectory: protective
281 |   provocatory: provocative
282 |   pruriency: prurience
283 |   psychal: psychical
284 |   punitory: punitive
285 |   quantitate: quantify
286 |   questionary: questionnaire
287 |   quiescency: quiescence
288 |   rabbin: rabbi
289 |   reasonability: reasonableness
290 |   recidivistic: recidivous
291 |   recriminative: recriminatory
292 |   recruital: recruitment
293 |   recurrency: recurrence
294 |   recusance: recusancy
295 |   recusation: recusal
296 |   recusement: recusal
297 |   redemptory: redemptive
298 |   referrable: referable
299 |   referrible: referable
300 |   refutatory: refutative
301 |   remitment: remittance
302 |   remittal: remission
303 |   renouncement: renunciation
304 |   renunciable: renounceable
305 |   reparatory: reparative
306 |   repudiative: repudiatory
307 |   requitement: requital
308 |   rescindment: rescission
309 |   restoral: restoration
310 |   reticency: reticence
311 |   reviewal: review
312 |   revisal: revision
313 |   revisional: revisionary
314 |   revolute: revolt
315 |   saliency: salience
316 |   salutiferous: salutary
317 |   sensatory: sensory
318 |   sessionary: sessional
319 |   shareowner: shareholder
320 |   sicklily: sickly
321 |   signator: signatory
322 |   slanderize: slander
323 |   societary: societal
324 |   sodomist: sodomite
325 |   solicitate: solicit
326 |   speculatory: speculative
327 |   spiritous: spirituous
328 |   statutorial: statutory
329 |   submergeable: submersible
330 |   submittal: submission
331 |   subtile: subtle
332 |   succuba: succubus
333 |   sufficience: sufficiency
334 |   suppliant: supplicant
335 |   surmisal: surmise
336 |   suspendible: suspendable
337 |   synthetize: synthesize
338 |   systemize: systematize
339 |   tactual: tactile
340 |   tangental: tangential
341 |   tautologous: tautological
342 |   tee-shirt: T-shirt
343 |   thenceforward: thenceforth
344 |   transiency: transience
345 |   transposal: transposition
346 |   transposal: transposition
347 |   unfrequent: infrequent
348 |   unreasonability: unreasonableness
349 |   unrevokable: irrevocable
350 |   unsubstantial: insubstantial
351 |   usurpature: usurpation
352 |   variative: variational
353 |   vegetive: vegetative
354 |   vindicative: vindictive
355 |   vituperous: vituperative
356 |   vociferant: vociferous
357 |   volitive: volitional
358 |   wolverene: wolverine
359 |   wolvish: wolfish
360 |   Zoroastrism: Zoroastrianism
361 | 
362 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/Nonwords.yml:
--------------------------------------------------------------------------------
 1 | extends: substitution
 2 | message: "Consider using '%s' instead of '%s'."
 3 | ignorecase: true
 4 | level: error
 5 | action:
 6 |   name: replace
 7 | swap:
 8 |   affrontery: effrontery
 9 |   analyzation: analysis
10 |   annoyment: annoyance
11 |   confirmant: confirmand
12 |   confirmants: confirmands
13 |   conversate: converse
14 |   crained: cranded
15 |   discomforture: discomfort|discomfiture
16 |   dispersement: disbursement|dispersal
17 |   doubtlessly: doubtless|undoubtedly
18 |   forebearance: forbearance
19 |   improprietous: improper
20 |   inclimate: inclement
21 |   inimicable: inimical
22 |   irregardless: regardless
23 |   minimalize: minimize
24 |   minimalized: minimized
25 |   minimalizes: minimizes
26 |   minimalizing: minimizing
27 |   optimalize: optimize
28 |   paralyzation: paralysis
29 |   pettifogger: pettifog
30 |   proprietous: proper
31 |   relative inexpense: relatively low price|affordability
32 |   seldomly: seldom
33 |   thusly: thus
34 |   uncategorically: categorically
35 |   undoubtably: undoubtedly|indubitably
36 |   unequivocable: unequivocal
37 |   unmercilessly: mercilessly
38 |   unrelentlessly: unrelentingly|relentlessly
39 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/Oxymorons.yml:
--------------------------------------------------------------------------------
 1 | extends: existence
 2 | message: "'%s' is an oxymoron."
 3 | ignorecase: true
 4 | level: error
 5 | tokens:
 6 |   - amateur expert
 7 |   - increasingly less
 8 |   - advancing backwards
 9 |   - alludes explicitly to
10 |   - explicitly alludes to
11 |   - totally obsolescent
12 |   - completely obsolescent
13 |   - generally always
14 |   - usually always
15 |   - increasingly less
16 |   - build down
17 |   - conspicuous absence
18 |   - exact estimate
19 |   - found missing
20 |   - intense apathy
21 |   - mandatory choice
22 |   - organized mess
23 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/P-Value.yml:
--------------------------------------------------------------------------------
1 | extends: existence
2 | message: "You should use more decimal places, unless '%s' is really true."
3 | ignorecase: true
4 | level: suggestion
5 | tokens:
6 |   - 'p = 0\.0{2,4}'
7 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/RASSyndrome.yml:
--------------------------------------------------------------------------------
 1 | extends: existence
 2 | message: "'%s' is redundant."
 3 | level: error
 4 | action:
 5 |   name: edit
 6 |   params:
 7 |    - split
 8 |    - ' '
 9 |    - '0'
10 | tokens:
11 |   - ABM missile
12 |   - ACT test
13 |   - ABM missiles
14 |   - ABS braking system
15 |   - ATM machine
16 |   - CD disc
17 |   - CPI Index
18 |   - GPS system
19 |   - GUI interface
20 |   - HIV virus
21 |   - ISBN number
22 |   - LCD display
23 |   - PDF format
24 |   - PIN number
25 |   - RAS syndrome
26 |   - RIP in peace
27 |   - please RSVP
28 |   - SALT talks
29 |   - SAT test
30 |   - UPC codes
31 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/Skunked.yml:
--------------------------------------------------------------------------------
 1 | extends: existence
 2 | message: "'%s' is a bit of a skunked term — impossible to use without issue."
 3 | ignorecase: true
 4 | level: error
 5 | tokens:
 6 |   - bona fides
 7 |   - deceptively
 8 |   - decimate
 9 |   - effete
10 |   - fulsome
11 |   - hopefully
12 |   - impassionate
13 |   - Thankfully
14 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/Spelling.yml:
--------------------------------------------------------------------------------
 1 | extends: consistency
 2 | message: "Inconsistent spelling of '%s'."
 3 | level: error
 4 | ignorecase: true
 5 | either:
 6 |   advisor: adviser
 7 |   centre: center
 8 |   colour: color
 9 |   emphasise: emphasize
10 |   finalise: finalize
11 |   focussed: focused
12 |   labour: labor
13 |   learnt: learned
14 |   organise: organize
15 |   organised: organized
16 |   organising: organizing
17 |   recognise: recognize
18 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/Typography.yml:
--------------------------------------------------------------------------------
 1 | extends: substitution
 2 | message: Consider using the '%s' symbol instead of '%s'.
 3 | level: error
 4 | nonword: true
 5 | swap:
 6 |   '\.\.\.': …
 7 |   '\([cC]\)': ©
 8 |   '\(TM\)': ™
 9 |   '\(tm\)': ™
10 |   '\([rR]\)': ®
11 |   '[0-9]+ ?x ?[0-9]+': ×
12 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/Uncomparables.yml:
--------------------------------------------------------------------------------
 1 | extends: existence
 2 | message: "'%s' is not comparable"
 3 | ignorecase: true
 4 | level: error
 5 | action:
 6 |   name: edit
 7 |   params:
 8 |    - split
 9 |    - ' '
10 |    - '1'
11 | raw:
12 |   - \b(?:absolutely|most|more|less|least|very|quite|largely|extremely|increasingly|kind of|mildy|hardly|greatly|sort of)\b\s*
13 | tokens:
14 |   - absolute
15 |   - adequate
16 |   - complete
17 |   - correct
18 |   - certain
19 |   - devoid
20 |   - entire
21 |   - 'false'
22 |   - fatal
23 |   - favorite
24 |   - final
25 |   - ideal
26 |   - impossible
27 |   - inevitable
28 |   - infinite
29 |   - irrevocable
30 |   - main
31 |   - manifest
32 |   - only
33 |   - paramount
34 |   - perfect
35 |   - perpetual
36 |   - possible
37 |   - preferable
38 |   - principal
39 |   - singular
40 |   - stationary
41 |   - sufficient
42 |   - 'true'
43 |   - unanimous
44 |   - unavoidable
45 |   - unbroken
46 |   - uniform
47 |   - unique
48 |   - universal
49 |   - void
50 |   - whole
51 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/Very.yml:
--------------------------------------------------------------------------------
1 | extends: existence
2 | message: "Remove '%s'."
3 | ignorecase: true
4 | level: error
5 | tokens:
6 |   - very
7 | 


--------------------------------------------------------------------------------
/.vale/styles/proselint/meta.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "author": "jdkato",
 3 |   "description": "A Vale-compatible implementation of the proselint linter.",
 4 |   "email": "support@errata.ai",
 5 |   "lang": "en",
 6 |   "url": "https://github.com/errata-ai/proselint/releases/latest/download/proselint.zip",
 7 |   "feed": "https://github.com/errata-ai/proselint/releases.atom",
 8 |   "issues": "https://github.com/errata-ai/proselint/issues/new",
 9 |   "license": "BSD-3-Clause",
10 |   "name": "proselint",
11 |   "sources": [
12 |     "https://github.com/amperser/proselint"
13 |   ],
14 |   "vale_version": ">=1.0.0",
15 |   "coverage": 0.0,
16 |   "version": "0.1.0"
17 | }
18 | 


--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 |   "recommendations": ["dbaeumer.vscode-eslint"]
3 | }
4 | 


--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "version": "0.2.0",
 3 |   "configurations": [
 4 |     {
 5 |       "name": "Debug Main Process",
 6 |       "type": "node",
 7 |       "request": "launch",
 8 |       "cwd": "${workspaceRoot}",
 9 |       "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
10 |       "windows": {
11 |         "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
12 |       },
13 |       "runtimeArgs": ["--sourcemap"],
14 |       "env": {
15 |         "REMOTE_DEBUGGING_PORT": "9222"
16 |       }
17 |     },
18 |     {
19 |       "name": "Debug Renderer Process",
20 |       "port": 9222,
21 |       "request": "attach",
22 |       "type": "chrome",
23 |       "webRoot": "${workspaceFolder}/src/renderer",
24 |       "timeout": 60000,
25 |       "presentation": {
26 |         "hidden": true
27 |       }
28 |     }
29 |   ],
30 |   "compounds": [
31 |     {
32 |       "name": "Debug All",
33 |       "configurations": ["Debug Main Process", "Debug Renderer Process"],
34 |       "presentation": {
35 |         "order": 1
36 |       }
37 |     }
38 |   ]
39 | }
40 | 


--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "[typescript]": {
 3 |     "editor.defaultFormatter": "esbenp.prettier-vscode"
 4 |   },
 5 |   "[javascript]": {
 6 |     "editor.defaultFormatter": "esbenp.prettier-vscode"
 7 |   },
 8 |   "[json]": {
 9 |     "editor.defaultFormatter": "esbenp.prettier-vscode"
10 |   },
11 |   "[svelte]": {
12 |     "editor.defaultFormatter": "svelte.svelte-vscode"
13 |   },
14 |   "svelte.enable-ts-plugin": true,
15 |   "eslint.validate": ["javascript", "javascriptreact", "svelte"]
16 | }
17 | 


--------------------------------------------------------------------------------
/.yamllint.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | extends: default
3 | rules:
4 |   truthy:
5 |     check-keys: false
6 | 


--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @gorillamoe
2 | 


--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
 1 | # Contributor Covenant Code of Conduct
 2 | 
 3 | ## Our Pledge
 4 | 
 5 | In the interest of fostering an open and welcoming environment, we as
 6 | contributors and maintainers pledge to make participation in our project and
 7 | our community a harassment-free experience for everyone, regardless of age, body
 8 | size, disability, ethnicity, gender identity and expression, level of experience,
 9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 | 
12 | ## Our Standards
13 | 
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 | 
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 | 
23 | Examples of unacceptable behavior by participants include:
24 | 
25 | - The use of sexualized language or imagery and unwelcome sexual attention or
26 |   advances
27 | - Trolling, insulting/derogatory comments, and personal or political attacks
28 | - Public or private harassment
29 | - Publishing others' private information, such as a physical or electronic
30 |   address, without explicit permission
31 | - Other conduct which could reasonably be considered inappropriate in a
32 |   professional setting
33 | 
34 | ## Our Responsibilities
35 | 
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 | 
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 | 
46 | ## Scope
47 | 
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community includes using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 | 
55 | ## Enforcement
56 | 
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting an individual maintainer on:
59 | 
60 | ### GitHub
61 | 
62 | - @gorillamoe
63 | 
64 | > (at `GitHub username` + `@github.com`).
65 | 
66 | ### Discord
67 | 
68 | - gorillamoe
69 | 
70 | All complaints will be reviewed and investigated and will result in a response that
71 | is deemed necessary and appropriate to the circumstances. The project team is
72 | obligated to maintain confidentiality with regard to the reporter of an incident.
73 | Further details of specific enforcement policies may be posted separately.
74 | 
75 | Project maintainers who do not follow or enforce the Code of Conduct in good
76 | faith may face temporary or permanent repercussions as determined by other
77 | members of the project's leadership.
78 | 
79 | ## Attribution
80 | 
81 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
82 | available at [https://contributor-covenant.org/version/1/4][version]
83 | 
84 | [homepage]: https://contributor-covenant.org
85 | [version]: https://contributor-covenant.org/version/1/4/
86 | 


--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
 1 | # Contributing to Bananas Screen Sharing
 2 | 
 3 | Thanks for checking out Bananas Screen Sharing!
 4 | We're excited to hear and learn from you.
 5 | 
 6 | We've put together the following guidelines to
 7 | help you figure out where you can best be helpful.
 8 | 
 9 | ## Table of Contents
10 | 
11 | 0. [Types of contributions we're looking for](#types-of-contributions-were-looking-for)
12 | 1. [Ground rules & expectations](#ground-rules--expectations)
13 | 2. [How to contribute](#how-to-contribute)
14 | 3. [Style guide](#style-guide)
15 | 4. [Documentation](#documentation)
16 | 5. [Code](#code)
17 | 6. [Setting up your environment](#setting-up-your-environment)
18 | 7. [Community](#community)
19 | 
20 | ## Types of contributions we're looking for
21 | 
22 | There are many ways you can directly contribute to Bananas Screen Sharing:
23 | 
24 | - Feature requests
25 | - Bug reports
26 | - Code contributions
27 | - Writing or editing documentation
28 | 
29 | ## Ground rules & expectations
30 | 
31 | Before we get started,
32 | here are a few things we expect from you (and that you should expect from others):
33 | 
34 | - Be kind and thoughtful in your conversations around this project.
35 |   We all come from different backgrounds and projects,
36 |   which means we likely have different perspectives on "how open source is done."
37 |   Try to listen to others rather than convince them that your way is correct.
38 | - Bananas is released with a [Contributor Code of Conduct](./CODE_OF_CONDUCT.md).
39 |   By participating in this project, you agree to abide by its terms.
40 | - Please ensure that your contribution passes all tests if you open a pull request.
41 |   If there are test failures, you will need to address them before we can merge your contribution.
42 | - When adding content, please consider if it is widely valuable.
43 |   Please don't add references or links to things you or your employer have created,
44 |   as others will do so if they appreciate it.
45 | 
46 | ## How to contribute
47 | 
48 | If you'd like to contribute,
49 | start by searching through the [pull requests](https://github.com/mistweaverco/bananas/pulls) to
50 | see whether someone else has raised a similar idea or question.
51 | 
52 | If you don't see your idea listed, and you think it fits into the goals of this guide, open a pull request.
53 | 
54 | ## Style guide
55 | 
56 | ### Documentation
57 | 
58 | If you're writing documentation,
59 | see the [style guide](.vale/styles) (which uses [vale](https://vale.sh)) to
60 | help your prose match the rest of the documentation.
61 | 
62 | ### Code
63 | 
64 | When writing code,
65 | please follow these configurations:
66 | 
67 | - [eslint](./eslintrc.cjs)
68 | - [EditorConfig](./.editorconfig)
69 | - [yaml-lint](./.yamllint.yaml)
70 | 
71 | Most of them are automatically checked by the CI,
72 | so you don't need to worry about them.
73 | 
74 | ## Community
75 | 
76 | Discussions about the Bananas take place on:
77 | 
78 | - This repository's [Issues](https://github.com/mistweaverco/bananas/issues) and
79 |   [Pull Requests](https://github.com/mistweaverco/bananas/pulls) sections
80 | - The [Bananas Discord server](https://discord.gg/BeN43eJVWS)
81 | 
82 | Anybody is welcome to join these conversations.
83 | 
84 | Wherever possible,
85 | do not take these conversations to private channels,
86 | including contacting the maintainers directly.
87 | 
88 | Keeping communication public means everybody can benefit and learn from the conversation.
89 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2024+ mistweaverco
 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 | 


--------------------------------------------------------------------------------
/PRIVACY.md:
--------------------------------------------------------------------------------
 1 | # Privacy Policy
 2 | 
 3 | Effective Date: 2024-12-03
 4 | 
 5 | Your privacy is important to us.
 6 | This Privacy Policy outlines how we handle and protect your information when you use
 7 | Bananas Screen Sharing (the "App").
 8 | 
 9 | By using the App, you agree to the terms of this Privacy Policy.
10 | 
11 | ### 1. Data Collection
12 | 
13 | We do not collect, store, or process any personal or usage data from users of the App.
14 | The App functions solely to establish a peer-to-peer connection between users
15 | No personal information, identifiers, or activity data are transmitted to us or any third-party;
16 | excluding stun and turn servers for negotiation of connection details like IP addresses and ports.
17 | 
18 | ### 2. Data Usage
19 | 
20 | The App interacts with stun and turn servers to establish a peer-to-peer connection between users.
21 | 
22 | ### 3. Third-Party Services
23 | 
24 | The App uses the official stun server provided by Google
25 | if not provided by the user.
26 | 
27 | This is required for the negotiation of connection details like IP addresses and ports,
28 | to establish a peer-to-peer connection between users.
29 | 
30 | ### 4. Security
31 | 
32 | Although we do not collect any data,
33 | we prioritize security in the development and maintenance of the App to
34 | ensure that your use of it remains safe.
35 | 
36 | The App only communicates with the stun or turn servers
37 | that you can configure yourself,
38 | for the purpose of establishing a peer-to-peer connection and
39 | does not expose your data to any other services or entities.
40 | 
41 | ### 5. User Consent
42 | 
43 | By using the App,
44 | you consent to the interaction between the configured stun or turn servers and the App,
45 | as described in this policy.
46 | 
47 | You understand that the App does not collect or store any personal data.
48 | 
49 | ### 6. Changes to the Privacy Policy
50 | 
51 | We reserve the right to modify this Privacy Policy at any time.
52 | Any changes will be posted on this page with an updated "Effective Date."
53 | 
54 | Your continued use of the App after changes to this Privacy Policy indicates
55 | your acceptance of the revised terms.
56 | 
57 | ### 7. Contact Us
58 | 
59 | If you have any questions or concerns about this Privacy Policy or the App,
60 | please contact us via filing an issue on the
61 | [GitHub repository](https://github.com/mistweaverco/bananas/issues/new).
62 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | <div align="center">
 2 | 
 3 | ![Bananas Screen Sharing Logo](logo.svg)
 4 | 
 5 | # Bananas Screen Sharing
 6 | 
 7 | [![Downloads](https://img.shields.io/github/downloads/mistweaverco/bananas/total.svg?style=for-the-badge)](https://getbananas.net/)
 8 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/mistweaverco/bananas?style=for-the-badge)](https://github.com/mistweaverco/bananas/releases/latest)
 9 | 
10 | [Install](#install) • [Website](https://getbananas.net/) • [Tutorial](https://getbananas.net/tutorial) • [Privacy Policy](./PRIVACY.md) • [Terms of Service](./TOS.md) • [Code of Conduct](./CODE_OF_CONDUCT.md)
11 | 
12 | <p></p>
13 | 
14 | Bananas Screen Sharing is a simple and
15 | easy-to-use screen sharing tool for Mac, Windows, and Linux.
16 | 
17 | It utilizes a peer-to-peer connection to share your screen with others,
18 | without the need for an account or any server infrastructure
19 | (except for the stun, turn and signaling servers that are needed for exchanging the initial connection information)
20 | 
21 | <p></p>
22 | 
23 | </div>
24 | 
25 | ## Install
26 | 
27 | Grab the latest release from the
28 | [GitHub releases page](https://github.com/mistweaverco/bananas/releases/latest).
29 | 
30 | Or if you are on Mac you can install it via homebrew with
31 | 
32 | ```shell
33 | brew install --cask bananas
34 | ```
35 | 


--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
 1 | # Security Policy
 2 | 
 3 | ## Supported Versions
 4 | 
 5 | Versions currently being supported with security updates.
 6 | 
 7 | | Version | Supported          |
 8 | | ------- | ------------------ |
 9 | | 0.0.x   | :white_check_mark: |
10 | 
11 | ## Reporting a Vulnerability
12 | 
13 | Security vulnerabilites ahould be communicated with
14 | the maintainers in private.
15 | 
16 | ### GitHub
17 | 
18 | - @gorillamoe
19 | 
20 | > (at `GitHub username` + `@github.com`).
21 | 
22 | ### Discord
23 | 
24 | - gorillamoe
25 | 


--------------------------------------------------------------------------------
/TOS.md:
--------------------------------------------------------------------------------
 1 | # Terms of Service
 2 | 
 3 | Terms of Service for Bananas Screen Sharing
 4 | Effective Date: 2024-12-03
 5 | 
 6 | These Terms of Service ("Terms") govern your use of the Bananas Screen Sharing (the "App").
 7 | 
 8 | By using the App, you agree to these Terms.
 9 | If you do not agree to these Terms, you may not use the App.
10 | 
11 | ### 1. Acceptance of Terms
12 | 
13 | By installing, accessing, or using the App, you agree to comply with these Terms.
14 | If you do not agree to these Terms,
15 | you must not use the App.
16 | 
17 | We reserve the right to modify these Terms at any time,
18 | and your continued use of the App after any such modifications indicates your acceptance of the new terms.
19 | 
20 | ### 2. License
21 | 
22 | See [LICENSE](LICENSE) for the license under which the App is provided.
23 | 
24 | ### 4. User Conduct
25 | 
26 | When using the App, you agree to:
27 | 
28 | Use the App solely for its intended purpose.
29 | Not attempt to interfere with the App's functionality or access any other users' information.
30 | 
31 | ### 5. Disclaimer of Warranties
32 | 
33 | The App is provided on an "as-is" and "as-available" basis,
34 | without any warranties of any kind. We do not guarantee that the App will be error-free,
35 | secure, or uninterrupted. You use the App at your own risk.
36 | 
37 | ### 6. Limitation of Liability
38 | 
39 | To the maximum extent permitted by law,
40 | we shall not be liable for any damages arising from the use or inability to use the App,
41 | including but not limited to direct, indirect, incidental, or consequential damages.
42 | This includes, without limitation, any loss of data,
43 | interruption of service, or
44 | issues resulting from third-party interactions (e.g., stun or turn servers).
45 | 
46 | ### 7. Termination
47 | 
48 | We reserve the right to terminate your access to the App at any time for any reason,
49 | including but not limited to your violation of these Terms.
50 | Upon termination, the rights and licenses granted to you under these Terms will immediately cease.
51 | 
52 | ### 8. Third-Party Services
53 | 
54 | The App may use third-party services, such as stun or turn servers,
55 | to facilitate its functionality.
56 | 
57 | ### 9. Governing Law
58 | 
59 | These Terms shall be governed and construed in accordance with the laws of Germany.
60 | 
61 | Any legal actions or disputes arising in connection with these Terms
62 | will be resolved in the courts of Germany.
63 | 
64 | ### 10. Changes to These Terms
65 | 
66 | We reserve the right to modify these Terms at any time.
67 | Any changes will be posted on this page with an updated "Effective Date."
68 | Your continued use of the App after changes to
69 | the Terms indicates your acceptance of the modified terms.
70 | 
71 | ### 11. Contact Us
72 | 
73 | If you have any questions or concerns about these Terms,
74 | please contact us via filing an issue on the
75 | [GitHub repository](https://github.com/mistweaverco/bananas/issues/new).
76 | 


--------------------------------------------------------------------------------
/assets/open-graph.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mistweaverco/bananas/4b03d3f0475dc2a9936da78580fcab500a8014cf/assets/open-graph.xcf


--------------------------------------------------------------------------------
/build/entitlements.mac.plist:
--------------------------------------------------------------------------------
 1 | <?xml version="1.0" encoding="UTF-8"?>
 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 3 | <plist version="1.0">
 4 |   <dict>
 5 |     <key>com.apple.security.cs.allow-jit</key>
 6 |     <true/>
 7 |     <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
 8 |     <true/>
 9 |     <key>com.apple.security.cs.allow-dyld-environment-variables</key>
10 |     <true/>
11 |     <key>com.apple.security.device.audio-input</key>
12 |     <true/>
13 |   </dict>
14 | </plist>
15 | 


--------------------------------------------------------------------------------
/build/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mistweaverco/bananas/4b03d3f0475dc2a9936da78580fcab500a8014cf/build/icon.icns


--------------------------------------------------------------------------------
/build/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mistweaverco/bananas/4b03d3f0475dc2a9936da78580fcab500a8014cf/build/icon.ico


--------------------------------------------------------------------------------
/build/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mistweaverco/bananas/4b03d3f0475dc2a9936da78580fcab500a8014cf/build/icon.png


--------------------------------------------------------------------------------
/build/icon_1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mistweaverco/bananas/4b03d3f0475dc2a9936da78580fcab500a8014cf/build/icon_1024.png


--------------------------------------------------------------------------------
/build/icon_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mistweaverco/bananas/4b03d3f0475dc2a9936da78580fcab500a8014cf/build/icon_128.png


--------------------------------------------------------------------------------
/build/icon_256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mistweaverco/bananas/4b03d3f0475dc2a9936da78580fcab500a8014cf/build/icon_256.png


--------------------------------------------------------------------------------
/build/icon_32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mistweaverco/bananas/4b03d3f0475dc2a9936da78580fcab500a8014cf/build/icon_32.png


--------------------------------------------------------------------------------
/build/icon_512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mistweaverco/bananas/4b03d3f0475dc2a9936da78580fcab500a8014cf/build/icon_512.png


--------------------------------------------------------------------------------
/build/icon_64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mistweaverco/bananas/4b03d3f0475dc2a9936da78580fcab500a8014cf/build/icon_64.png


--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mistweaverco/bananas/4b03d3f0475dc2a9936da78580fcab500a8014cf/bun.lockb


--------------------------------------------------------------------------------
/dev-app-update.yml:
--------------------------------------------------------------------------------
1 | ---
2 | provider: generic
3 | url: https://auto-updates.getbananas.net
4 | updaterCacheDirName: bananas-updater
5 | 


--------------------------------------------------------------------------------
/electron-builder.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | appId: net.getbananas.desktop
 3 | productName: bananas
 4 | directories:
 5 |   buildResources: build
 6 | files:
 7 |   - '!**/.vscode/*'
 8 |   - '!src/*'
 9 |   - '!electron.vite.config.{js,ts,mjs,cjs}'
10 |   - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
11 |   - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
12 |   - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
13 | asarUnpack:
14 |   - resources/**
15 | afterSign: scripts/notarize.js
16 | win:
17 |   executableName: bananas
18 |   icon: build/icon.ico
19 | nsis:
20 |   artifactName: ${name}-setup_${arch}.${ext}
21 |   shortcutName: ${productName}
22 |   uninstallDisplayName: ${productName}
23 |   createDesktopShortcut: always
24 |   installerIcon: build/icon.ico
25 |   oneClick: false
26 |   perMachine: false
27 |   allowToChangeInstallationDirectory: true
28 | mac:
29 |   entitlementsInherit: build/entitlements.mac.plist
30 |   extendInfo:
31 |     - NSMicrophoneUsageDescription: Application requests access to the device's microphone.
32 |     - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
33 |     - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
34 |   hardenedRuntime: true
35 |   notarize: false
36 |   icon: build/icon.icns
37 |   target:
38 |     - target: dmg
39 |       arch:
40 |         - universal
41 | dmg:
42 |   artifactName: ${name}_${arch}.${ext}
43 |   icon: build/icon.icns
44 | linux:
45 |   target:
46 |     - AppImage
47 |     - snap
48 |     - flatpak
49 |     - deb
50 |   maintainer: getbananas.net
51 |   category: Utility
52 |   icon: build/
53 | appImage:
54 |   artifactName: ${name}_${arch}.${ext}
55 | snap:
56 |   artifactName: ${name}_${arch}.${ext}
57 | flatpak:
58 |   artifactName: ${name}_${arch}.${ext}
59 | deb:
60 |   artifactName: ${name}_${arch}.${ext}
61 | npmRebuild: false
62 | publish:
63 |   provider: generic
64 |   url: https://auto-updates.getbananas.net/
65 | 


--------------------------------------------------------------------------------
/electron.vite.config.ts:
--------------------------------------------------------------------------------
 1 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
 2 | import { svelte } from '@sveltejs/vite-plugin-svelte'
 3 | 
 4 | export default defineConfig({
 5 |   main: {
 6 |     build: {
 7 |       rollupOptions: {
 8 |         input: {
 9 |           index: 'src/main/index.ts'
10 |         }
11 |       }
12 |     },
13 |     plugins: [externalizeDepsPlugin()]
14 |   },
15 |   preload: {
16 |     build: {
17 |       rollupOptions: {
18 |         input: {
19 |           index: 'src/preload/index.ts',
20 |           cursors: 'src/preload/cursors.ts'
21 |         }
22 |       }
23 |     },
24 |     plugins: [externalizeDepsPlugin()]
25 |   },
26 |   renderer: {
27 |     build: {
28 |       rollupOptions: {
29 |         input: {
30 |           index: 'src/renderer/index.html',
31 |           cursors: 'src/renderer/cursors.html'
32 |         }
33 |       }
34 |     },
35 |     plugins: [svelte()]
36 |   }
37 | })
38 | 


--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mistweaverco/bananas/4b03d3f0475dc2a9936da78580fcab500a8014cf/logo.png


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "bananas",
 3 |   "version": "0.0.22",
 4 |   "description": "Bananas Screen Sharing is a simple and easy-to-use screen sharing tool for Mac, Windows, and Linux.",
 5 |   "main": "./out/main/index.js",
 6 |   "author": {
 7 |     "name": "Marco Kellershoff",
 8 |     "email": "marco@kellershoff.net"
 9 |   },
10 |   "license": "MIT",
11 |   "homepage": "https://getbananas.net",
12 |   "scripts": {
13 |     "format": "prettier --plugin prettier-plugin-svelte --write .",
14 |     "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
15 |     "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
16 |     "svelte-check": "svelte-check --tsconfig ./tsconfig.json",
17 |     "typecheck": "bun run typecheck:node && bun run svelte-check",
18 |     "start": "electron-vite preview",
19 |     "dev": "electron-vite dev",
20 |     "build": "bun run typecheck && electron-vite build",
21 |     "postinstall": "electron-builder install-app-deps",
22 |     "build:unpack": "bun run build && electron-builder --dir",
23 |     "build:windows": "bun run build && electron-builder --win --publish never",
24 |     "build:macos": "./scripts/build.sh PLATFORM=macos",
25 |     "build:linux": "./scripts/build.sh PLATFORM=linux",
26 |     "build:linux-arm64": "./scripts/build.sh PLATFORM=linux-arm64",
27 |     "build:linux-debug": "./scripts/build.sh PLATFORM=linux-debug",
28 |     "node:packages:update": "ncu -u",
29 |     "node:packages:upgrade": "ncu -u && bun install",
30 |     "translations": "typesafe-i18n --no-watch",
31 |     "translations:watch": "typesafe-i18n"
32 |   },
33 |   "dependencies": {
34 |     "@electron-toolkit/preload": "3.0.1",
35 |     "@electron-toolkit/utils": "3.0.0",
36 |     "@sweetalert2/theme-bulma": "5.0.18",
37 |     "electron-settings": "4.0.4",
38 |     "electron-updater": "6.3.9",
39 |     "sweetalert2": "11.14.5",
40 |     "typesafe-i18n": "5.26.2"
41 |   },
42 |   "devDependencies": {
43 |     "@electron-toolkit/eslint-config-prettier": "^2.0.0",
44 |     "@electron-toolkit/eslint-config-ts": "^2.0.0",
45 |     "@electron-toolkit/tsconfig": "^1.0.1",
46 |     "@fortawesome/fontawesome-free": "6.6.0",
47 |     "@mistweaverco/electron-notarize-async": "0.0.4",
48 |     "@sveltejs/adapter-node": "5.2.9",
49 |     "@sveltejs/vite-plugin-svelte": "3.1.2",
50 |     "@types/bun": "^1.1.14",
51 |     "bulma": "1.0.2",
52 |     "dotenv": "16.4.5",
53 |     "electron": "^31.0.2",
54 |     "electron-builder": "^24.13.3",
55 |     "electron-log": "5.2.3",
56 |     "electron-vite": "^2.3.0",
57 |     "eslint": "^8.57.0",
58 |     "eslint-plugin-svelte": "^2.41.0",
59 |     "kit-monorepo": "github:sveltejs/kit",
60 |     "prettier": "^3.3.2",
61 |     "prettier-plugin-svelte": "^3.2.5",
62 |     "svelte": "4.2.19",
63 |     "svelte-awesome-color-picker": "3.1.4",
64 |     "svelte-check": "3.8.6",
65 |     "tslib": "^2.6.3",
66 |     "typescript": "^5.5.2",
67 |     "vite": "^5.3.1"
68 |   }
69 | }
70 | 


--------------------------------------------------------------------------------
/resources/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mistweaverco/bananas/4b03d3f0475dc2a9936da78580fcab500a8014cf/resources/icon.png


--------------------------------------------------------------------------------
/scripts/build.sh:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env bash
 2 | 
 3 | if [ -z "$VERSION" ]; then echo "Error: VERSION is not set"; exit 1; fi
 4 | if [ -z "$PLATFORM" ]; then echo "Error: PLATFORM is not set"; exit 1; fi
 5 | 
 6 | update_package_json_version() {
 7 |   local tmp
 8 |   tmp=$(mktemp)
 9 |   jq --arg v "$VERSION" '.version = $v' package.json > "$tmp" && mv "$tmp" package.json
10 | }
11 | 
12 | update_package_json_version
13 | 
14 | build_windows() {
15 |   bun run build && ./node_modules/.bin/electron-builder --win --publish never
16 | }
17 | 
18 | build_linux() {
19 |   bun run build && ./node_modules/.bin/electron-builder --linux --publish never
20 | }
21 | 
22 | build_linux_arm64() {
23 |   # NOTE:
24 |   # One might imagine we could use `electron-builder --arm64` here, but
25 |   # it doesn't work as expected.
26 |   # There is an issue when building snap packages.
27 |   # Instead, we build each target individually.
28 |   bun run build && ./node_modules/.bin/electron-builder --linux deb --publish never --arm64 && \
29 |     bun run build && ./node_modules/.bin/electron-builder --linux flatpak --publish never --arm64 && \
30 |     bun run build && ./node_modules/.bin/electron-builder --linux appimage --publish never --arm64
31 | }
32 | 
33 | build_linux_debug() {
34 |   bun run build && ./node_modules/.bin/electron-builder --linux deb --publish never
35 | }
36 | 
37 | build_macos() {
38 |   bun run build && ./node_modules/.bin/electron-builder --mac --publish never
39 | }
40 | 
41 | case $PLATFORM in
42 |   "linux")
43 |     build_linux
44 |     ;;
45 |   "linux-arm64")
46 |     build_linux_arm64
47 |     ;;
48 |   "linux-debug")
49 |     build_linux_debug
50 |     ;;
51 |   "macos")
52 |     build_macos
53 |     ;;
54 |   "windows")
55 |     build_windows
56 |     ;;
57 |   *)
58 |     echo "Error: PLATFORM $PLATFORM is not supported"
59 |     exit 1
60 |     ;;
61 | esac
62 | 


--------------------------------------------------------------------------------
/scripts/notarize.js:
--------------------------------------------------------------------------------
 1 | require('dotenv').config()
 2 | const { notarize } = require('@mistweaverco/electron-notarize-async')
 3 | 
 4 | exports.default = async function notarizing(context) {
 5 |   const { electronPlatformName, appOutDir } = context
 6 |   if (electronPlatformName !== 'darwin') {
 7 |     return
 8 |   }
 9 | 
10 |   const appName = context.packager.appInfo.productFilename
11 | 
12 |   return await notarize({
13 |     tool: 'notarytool',
14 |     teamId: process.env.APPLE_TEAM_ID,
15 |     appPath: `${appOutDir}/${appName}.app`,
16 |     appleId: process.env.APPLE_ID,
17 |     appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD,
18 |     webhook: process.env.APPLE_NOTARIZATION_WEBHOOK_URL
19 |   })
20 | }
21 | 


--------------------------------------------------------------------------------
/scripts/release.sh:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env bash
  2 | 
  3 | if [ -z "$VERSION" ]; then echo "Error: VERSION is not set"; exit 1; fi
  4 | if [ -z "$PLATFORM" ]; then echo "Error: PLATFORM is not set"; exit 1; fi
  5 | 
  6 | BIN_NAME="bananas"
  7 | RELEASE_ACTION="create"
  8 | GH_TAG="v$VERSION"
  9 | FILES=()
 10 | 
 11 | LINUX_FILES=(
 12 |   "dist/${BIN_NAME}_amd64.deb"
 13 |   "dist/${BIN_NAME}_amd64.snap"
 14 |   "dist/${BIN_NAME}_x86_64.AppImage"
 15 |   "dist/${BIN_NAME}_x86_64.flatpak"
 16 | )
 17 | 
 18 | LINUX_ARM64_FILES=(
 19 |   "dist/${BIN_NAME}_arm64.deb"
 20 |   "dist/${BIN_NAME}_arm64.AppImage"
 21 |   "dist/${BIN_NAME}_aarch64.flatpak"
 22 | )
 23 | 
 24 | WINDOWS_FILES=(
 25 |   "dist/${BIN_NAME}-setup_x64.exe"
 26 | )
 27 | 
 28 | MACOS_FILES=(
 29 |   "dist/${BIN_NAME}_universal.dmg"
 30 | )
 31 | 
 32 | set_release_action() {
 33 |   if gh release view "$GH_TAG" --json id --jq .id > /dev/null 2>&1; then
 34 |     echo "Release $GH_TAG already exists, updating it"
 35 |     RELEASE_ACTION="edit"
 36 |   else
 37 |     echo "Release $GH_TAG does not exist, creating it"
 38 |     RELEASE_ACTION="create"
 39 |   fi
 40 | }
 41 | 
 42 | check_files_exist() {
 43 |   files=()
 44 |   for file in "${FILES[@]}"; do
 45 |     if [ ! -f "$file" ]; then
 46 |       files+=("$file")
 47 |     fi
 48 |   done
 49 |   if [ ${#files[@]} -gt 0 ]; then
 50 |     echo "Error: the following files do not exist:"
 51 |     for file in "${files[@]}"; do
 52 |       printf " - %s\n" "$file"
 53 |     done
 54 |     echo "This is the content of the dist directory:"
 55 |     ls -l dist/
 56 |     exit 1
 57 |   fi
 58 | }
 59 | 
 60 | set_files_based_on_platform() {
 61 |   case $PLATFORM in
 62 |     linux)
 63 |       FILES=("${LINUX_FILES[@]}")
 64 |       ;;
 65 |     linux-arm64)
 66 |       FILES=("${LINUX_ARM64_FILES[@]}")
 67 |       ;;
 68 |     windows)
 69 |       FILES=("${WINDOWS_FILES[@]}")
 70 |       ;;
 71 |     macos)
 72 |       FILES=("${MACOS_FILES[@]}")
 73 |       ;;
 74 |     *)
 75 |       echo "Error: PLATFORM $PLATFORM is not supported"
 76 |       exit 1
 77 |       ;;
 78 |   esac
 79 | }
 80 | 
 81 | print_files() {
 82 |   echo "Files to upload:"
 83 |   for file in "${FILES[@]}"; do
 84 |     printf " - %s\n" "$file"
 85 |   done
 86 | }
 87 | 
 88 | do_gh_release() {
 89 |   if [ "$RELEASE_ACTION" == "edit" ]; then
 90 |     if [ -z "$REPLACE" ]; then
 91 |       echo "Trying to upload files to existing release $GH_TAG"
 92 |       print_files
 93 |       gh release upload "$GH_TAG" "${FILES[@]}"
 94 |     else
 95 |       echo "Overwriting existing release $GH_TAG"
 96 |       print_files
 97 |       gh release upload --clobber "$GH_TAG" "${FILES[@]}"
 98 |     fi
 99 |   else
100 |     echo "Creating new release $GH_TAG"
101 |     print_files
102 |     gh release create --generate-notes "$GH_TAG" "${FILES[@]}"
103 |   fi
104 | }
105 | 
106 | release() {
107 |   set_release_action
108 |   set_files_based_on_platform
109 |   check_files_exist
110 |   do_gh_release
111 | }
112 | 
113 | release
114 | 


--------------------------------------------------------------------------------
/scripts/set-version.sh:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env bash
 2 | #
 3 | if [ -z "$VERSION" ]; then echo "Error: VERSION is not set"; exit 1; fi
 4 | 
 5 | update_package_json_version() {
 6 |   local tmp
 7 |   tmp=$(mktemp)
 8 |   jq --arg v "$VERSION" '.version = $v' package.json > "$tmp" && mv "$tmp" package.json
 9 | }
10 | 
11 | update_package_json_version
12 | 


--------------------------------------------------------------------------------
/src/i18n/de/index.ts:
--------------------------------------------------------------------------------
 1 | import type { BaseTranslation } from '../i18n-types'
 2 | 
 3 | const en = {
 4 |   about: 'Über',
 5 |   advanced: 'Erweitert',
 6 |   basic: 'Grundlegend',
 7 |   cancel: 'Abbrechen',
 8 |   choose_a_color: 'Wähle eine Farbe',
 9 |   code_of_conduct: 'Verhaltenskodex',
10 |   color: 'Farbe',
11 |   connect: 'Verbinden',
12 |   connection_established: 'Verbindung hergestellt',
13 |   copy_my_connection_string: 'Kopiere meine Verbindungszeichenfolge',
14 |   disconnect: 'Trennen',
15 |   fullscreen: 'Vollbild',
16 |   host_a_session: 'Eine Sitzung hosten',
17 |   host_connection_string: 'Host-Verbindungszeichenfolge',
18 |   hosting_a_session: 'Eine Sitzung hosten',
19 |   is_microphone_active_on_connect: 'Ist das Mikrofon bei der Verbindung aktiviert?',
20 |   join_a_session: 'Einer Sitzung beitreten',
21 |   joined_a_session: 'Einer Sitzung beigetreten',
22 |   language: 'Sprache',
23 |   language_description:
24 |     'Wähle deine bevorzugte Sprache (nach dem Ändern der Sprache musst du die App neu starten)',
25 |   media: 'Media',
26 |   microphone_active: 'Mikrofon aktiv',
27 |   microphone_inactive: 'Mikrofon inaktiv',
28 |   not_streaming_your_display: 'Dein Bildschirm wird nicht gestreamt',
29 |   participant_connection_string: 'Teilnehmer-Verbindungszeichenfolge',
30 |   privacty_policy: 'Privatsphärenrichtlinie',
31 |   remote_cursors_disabled: 'Remote cursors deaktiviert',
32 |   remote_cursors_enabled: 'Remote cursors aktiviert',
33 |   remote_screen: 'Remote screen',
34 |   report_a_bug: 'Einen Fehler melden',
35 |   save: 'Speichern',
36 |   see_the_code: 'Sieh dir den Code an',
37 |   session_started: 'Sitzung gestartet',
38 |   settings: 'Einstellungen',
39 |   shoulders_of_giants: 'Auf den Schultern von Giganten',
40 |   shoulders_of_giants_description:
41 |     'Bananas Screen Sharing baut auf den folgenden Open-Source-Projekten auf (in keiner bestimmten Reihenfolge)',
42 |   start_a_new_session: 'Eine neue Sitzung starten',
43 |   streaming_your_display: 'Dein Bildschirm wird gestreamt',
44 |   stun_turn_server_objects: 'STUN/TURN Objekte (getrennt durch Zeilenumbruch)',
45 |   terms_of_service: 'Nutzungsbedingungen',
46 |   username: 'Benutzername',
47 |   website: 'Website',
48 |   zoom_in: 'Hineinzoomen',
49 |   zoom_out: 'Herauszoomen'
50 | } satisfies BaseTranslation
51 | 
52 | export default en
53 | 


--------------------------------------------------------------------------------
/src/i18n/en/index.ts:
--------------------------------------------------------------------------------
 1 | import type { BaseTranslation } from '../i18n-types'
 2 | 
 3 | const en = {
 4 |   about: 'About',
 5 |   advanced: 'Advanced',
 6 |   basic: 'Basic',
 7 |   cancel: 'Cancel',
 8 |   choose_a_color: 'Choose a color',
 9 |   code_of_conduct: 'Code of conduct',
10 |   color: 'Color',
11 |   connect: 'Connect',
12 |   connection_established: 'Connection established',
13 |   copy_my_connection_string: 'Copy my connection string',
14 |   disconnect: 'Disconnect',
15 |   fullscreen: 'Fullscreen',
16 |   host_a_session: 'Host a session',
17 |   host_connection_string: 'Host connection string',
18 |   hosting_a_session: 'Hosting a session',
19 |   is_microphone_active_on_connect: 'Is microphone active on connect',
20 |   join_a_session: 'Join a session',
21 |   joined_a_session: 'Joined a session',
22 |   language: 'Language',
23 |   language_description:
24 |     'Choose your preferred language (after changing the language, you need to restart the app)',
25 |   media: 'Media',
26 |   microphone_active: 'Microphone active',
27 |   microphone_inactive: 'Microphone inactive',
28 |   not_streaming_your_display: 'Not streaming your display',
29 |   participant_connection_string: 'Participant connection string',
30 |   privacty_policy: 'Privacy policy',
31 |   remote_cursors_disabled: 'Remote cursors disabled',
32 |   remote_cursors_enabled: 'Remote cursors enabled',
33 |   remote_screen: 'Remote screen',
34 |   report_a_bug: 'Report a bug',
35 |   save: 'Save',
36 |   see_the_code: 'See the code',
37 |   session_started: 'Session started',
38 |   settings: 'Settings',
39 |   shoulders_of_giants: 'Shoulders of giants',
40 |   shoulders_of_giants_description:
41 |     'Bananas Screen Sharing is built on top of the following open-source projects (in no particular order)',
42 |   start_a_new_session: 'Start a new session',
43 |   streaming_your_display: 'Streaming your display',
44 |   stun_turn_server_objects: 'STUN/TURN Server Objects (separated by new lines)',
45 |   terms_of_service: 'Terms of service',
46 |   username: 'Username',
47 |   website: 'Website',
48 |   zoom_in: 'Zoom in',
49 |   zoom_out: 'Zoom out'
50 | } satisfies BaseTranslation
51 | 
52 | export default en
53 | 


--------------------------------------------------------------------------------
/src/i18n/formatters.ts:
--------------------------------------------------------------------------------
 1 | import type { FormattersInitializer } from 'typesafe-i18n'
 2 | import type { Locales, Formatters } from './i18n-types'
 3 | 
 4 | export const initFormatters: FormattersInitializer<Locales, Formatters> = (locale: Locales) => {
 5 |   const formatters: Formatters = {
 6 |     // add your formatter functions here
 7 |   }
 8 | 
 9 |   console.log(locale)
10 | 
11 |   return formatters
12 | }
13 | 


--------------------------------------------------------------------------------
/src/i18n/fr/index.ts:
--------------------------------------------------------------------------------
 1 | import type { BaseTranslation } from '../i18n-types'
 2 | 
 3 | const fr = {
 4 |   about: 'À propos',
 5 |   advanced: 'Avancé',
 6 |   basic: 'De base',
 7 |   cancel: 'Annuler',
 8 |   choose_a_color: 'Choisir une couleur',
 9 |   code_of_conduct: 'Code de conduite',
10 |   color: 'Couleur',
11 |   connect: 'Connecter',
12 |   connection_established: 'Connexion établie',
13 |   copy_my_connection_string: 'Copier ma chaîne de connexion',
14 |   disconnect: 'Déconnecter',
15 |   fullscreen: 'Plein écran',
16 |   host_a_session: 'Héberger une session',
17 |   host_connection_string: 'Chaîne de connexion de l’hôte',
18 |   hosting_a_session: 'Hébergement d’une session',
19 |   is_microphone_active_on_connect: 'Le microphone est-il actif à la connexion',
20 |   join_a_session: 'Rejoindre une session',
21 |   joined_a_session: 'Session rejointe',
22 |   language: 'Langue',
23 |   language_description:
24 |     "Choisissez votre langue préférée (après avoir changé la langue, vous devez redémarrer l'application)",
25 |   media: 'Médias',
26 |   microphone_active: 'Microphone actif',
27 |   microphone_inactive: 'Microphone inactif',
28 |   not_streaming_your_display: 'Ne diffuse pas votre écran',
29 |   participant_connection_string: 'Chaîne de connexion du participant',
30 |   privacty_policy: 'Politique de confidentialité',
31 |   remote_cursors_disabled: 'Curseurs distants désactivés',
32 |   remote_cursors_enabled: 'Curseurs distants activés',
33 |   remote_screen: 'Écran distant',
34 |   report_a_bug: 'Signaler un bug',
35 |   save: 'Enregistrer',
36 |   see_the_code: 'Voir le code',
37 |   session_started: 'Session commencée',
38 |   settings: 'Paramètres',
39 |   shoulders_of_giants: 'Épaules des géants',
40 |   shoulders_of_giants_description:
41 |     'Bananas Screen Sharing est construit sur les projets open-source suivants (dans un ordre aléatoire)',
42 |   start_a_new_session: 'Commencer une nouvelle session',
43 |   streaming_your_display: 'Diffusion de votre écran',
44 |   stun_turn_server_objects: 'Objets du serveur STUN/TURN (séparés par des nouvelles lignes)',
45 |   terms_of_service: 'Conditions de service',
46 |   username: 'Nom d’utilisateur',
47 |   website: 'Site web',
48 |   zoom_in: 'Zoomer',
49 |   zoom_out: 'Dézoomer'
50 | } satisfies BaseTranslation
51 | 
52 | export default fr
53 | 


--------------------------------------------------------------------------------
/src/i18n/i18n-svelte.ts:
--------------------------------------------------------------------------------
 1 | // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
 2 | /* eslint-disable */
 3 | 
 4 | import { initI18nSvelte } from 'typesafe-i18n/svelte'
 5 | import type { Formatters, Locales, TranslationFunctions, Translations } from './i18n-types'
 6 | import { loadedFormatters, loadedLocales } from './i18n-util'
 7 | 
 8 | const { locale, LL, setLocale } = initI18nSvelte<
 9 |   Locales,
10 |   Translations,
11 |   TranslationFunctions,
12 |   Formatters
13 | >(loadedLocales, loadedFormatters)
14 | 
15 | export { locale, LL, setLocale }
16 | 
17 | export default LL
18 | 


--------------------------------------------------------------------------------
/src/i18n/i18n-types.ts:
--------------------------------------------------------------------------------
  1 | // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
  2 | /* eslint-disable */
  3 | import type { BaseTranslation as BaseTranslationType, LocalizedString } from 'typesafe-i18n'
  4 | 
  5 | export type BaseTranslation = BaseTranslationType
  6 | export type BaseLocale = 'en'
  7 | 
  8 | export type Locales = 'de' | 'en' | 'fr' | 'zh'
  9 | 
 10 | export type Translation = RootTranslation
 11 | 
 12 | export type Translations = RootTranslation
 13 | 
 14 | type RootTranslation = {
 15 |   /**
 16 |    * A​b​o​u​t
 17 |    */
 18 |   about: string
 19 |   /**
 20 |    * A​d​v​a​n​c​e​d
 21 |    */
 22 |   advanced: string
 23 |   /**
 24 |    * B​a​s​i​c
 25 |    */
 26 |   basic: string
 27 |   /**
 28 |    * C​a​n​c​e​l
 29 |    */
 30 |   cancel: string
 31 |   /**
 32 |    * C​h​o​o​s​e​ ​a​ ​c​o​l​o​r
 33 |    */
 34 |   choose_a_color: string
 35 |   /**
 36 |    * C​o​d​e​ ​o​f​ ​c​o​n​d​u​c​t
 37 |    */
 38 |   code_of_conduct: string
 39 |   /**
 40 |    * C​o​l​o​r
 41 |    */
 42 |   color: string
 43 |   /**
 44 |    * C​o​n​n​e​c​t
 45 |    */
 46 |   connect: string
 47 |   /**
 48 |    * C​o​n​n​e​c​t​i​o​n​ ​e​s​t​a​b​l​i​s​h​e​d
 49 |    */
 50 |   connection_established: string
 51 |   /**
 52 |    * C​o​p​y​ ​m​y​ ​c​o​n​n​e​c​t​i​o​n​ ​s​t​r​i​n​g
 53 |    */
 54 |   copy_my_connection_string: string
 55 |   /**
 56 |    * D​i​s​c​o​n​n​e​c​t
 57 |    */
 58 |   disconnect: string
 59 |   /**
 60 |    * F​u​l​l​s​c​r​e​e​n
 61 |    */
 62 |   fullscreen: string
 63 |   /**
 64 |    * H​o​s​t​ ​a​ ​s​e​s​s​i​o​n
 65 |    */
 66 |   host_a_session: string
 67 |   /**
 68 |    * H​o​s​t​ ​c​o​n​n​e​c​t​i​o​n​ ​s​t​r​i​n​g
 69 |    */
 70 |   host_connection_string: string
 71 |   /**
 72 |    * H​o​s​t​i​n​g​ ​a​ ​s​e​s​s​i​o​n
 73 |    */
 74 |   hosting_a_session: string
 75 |   /**
 76 |    * I​s​ ​m​i​c​r​o​p​h​o​n​e​ ​a​c​t​i​v​e​ ​o​n​ ​c​o​n​n​e​c​t
 77 |    */
 78 |   is_microphone_active_on_connect: string
 79 |   /**
 80 |    * J​o​i​n​ ​a​ ​s​e​s​s​i​o​n
 81 |    */
 82 |   join_a_session: string
 83 |   /**
 84 |    * J​o​i​n​e​d​ ​a​ ​s​e​s​s​i​o​n
 85 |    */
 86 |   joined_a_session: string
 87 |   /**
 88 |    * L​a​n​g​u​a​g​e
 89 |    */
 90 |   language: string
 91 |   /**
 92 |    * C​h​o​o​s​e​ ​y​o​u​r​ ​p​r​e​f​e​r​r​e​d​ ​l​a​n​g​u​a​g​e​ ​(​a​f​t​e​r​ ​c​h​a​n​g​i​n​g​ ​t​h​e​ ​l​a​n​g​u​a​g​e​,​ ​y​o​u​ ​n​e​e​d​ ​t​o​ ​r​e​s​t​a​r​t​ ​t​h​e​ ​a​p​p​)
 93 |    */
 94 |   language_description: string
 95 |   /**
 96 |    * M​e​d​i​a
 97 |    */
 98 |   media: string
 99 |   /**
100 |    * M​i​c​r​o​p​h​o​n​e​ ​a​c​t​i​v​e
101 |    */
102 |   microphone_active: string
103 |   /**
104 |    * M​i​c​r​o​p​h​o​n​e​ ​i​n​a​c​t​i​v​e
105 |    */
106 |   microphone_inactive: string
107 |   /**
108 |    * N​o​t​ ​s​t​r​e​a​m​i​n​g​ ​y​o​u​r​ ​d​i​s​p​l​a​y
109 |    */
110 |   not_streaming_your_display: string
111 |   /**
112 |    * P​a​r​t​i​c​i​p​a​n​t​ ​c​o​n​n​e​c​t​i​o​n​ ​s​t​r​i​n​g
113 |    */
114 |   participant_connection_string: string
115 |   /**
116 |    * P​r​i​v​a​c​y​ ​p​o​l​i​c​y
117 |    */
118 |   privacty_policy: string
119 |   /**
120 |    * R​e​m​o​t​e​ ​c​u​r​s​o​r​s​ ​d​i​s​a​b​l​e​d
121 |    */
122 |   remote_cursors_disabled: string
123 |   /**
124 |    * R​e​m​o​t​e​ ​c​u​r​s​o​r​s​ ​e​n​a​b​l​e​d
125 |    */
126 |   remote_cursors_enabled: string
127 |   /**
128 |    * R​e​m​o​t​e​ ​s​c​r​e​e​n
129 |    */
130 |   remote_screen: string
131 |   /**
132 |    * R​e​p​o​r​t​ ​a​ ​b​u​g
133 |    */
134 |   report_a_bug: string
135 |   /**
136 |    * S​a​v​e
137 |    */
138 |   save: string
139 |   /**
140 |    * S​e​e​ ​t​h​e​ ​c​o​d​e
141 |    */
142 |   see_the_code: string
143 |   /**
144 |    * S​e​s​s​i​o​n​ ​s​t​a​r​t​e​d
145 |    */
146 |   session_started: string
147 |   /**
148 |    * S​e​t​t​i​n​g​s
149 |    */
150 |   settings: string
151 |   /**
152 |    * S​h​o​u​l​d​e​r​s​ ​o​f​ ​g​i​a​n​t​s
153 |    */
154 |   shoulders_of_giants: string
155 |   /**
156 |    * B​a​n​a​n​a​s​ ​S​c​r​e​e​n​ ​S​h​a​r​i​n​g​ ​i​s​ ​b​u​i​l​t​ ​o​n​ ​t​o​p​ ​o​f​ ​t​h​e​ ​f​o​l​l​o​w​i​n​g​ ​o​p​e​n​-​s​o​u​r​c​e​ ​p​r​o​j​e​c​t​s​ ​(​i​n​ ​n​o​ ​p​a​r​t​i​c​u​l​a​r​ ​o​r​d​e​r​)
157 |    */
158 |   shoulders_of_giants_description: string
159 |   /**
160 |    * S​t​a​r​t​ ​a​ ​n​e​w​ ​s​e​s​s​i​o​n
161 |    */
162 |   start_a_new_session: string
163 |   /**
164 |    * S​t​r​e​a​m​i​n​g​ ​y​o​u​r​ ​d​i​s​p​l​a​y
165 |    */
166 |   streaming_your_display: string
167 |   /**
168 |    * S​T​U​N​/​T​U​R​N​ ​S​e​r​v​e​r​ ​O​b​j​e​c​t​s​ ​(​s​e​p​a​r​a​t​e​d​ ​b​y​ ​n​e​w​ ​l​i​n​e​s​)
169 |    */
170 |   stun_turn_server_objects: string
171 |   /**
172 |    * T​e​r​m​s​ ​o​f​ ​s​e​r​v​i​c​e
173 |    */
174 |   terms_of_service: string
175 |   /**
176 |    * U​s​e​r​n​a​m​e
177 |    */
178 |   username: string
179 |   /**
180 |    * W​e​b​s​i​t​e
181 |    */
182 |   website: string
183 |   /**
184 |    * Z​o​o​m​ ​i​n
185 |    */
186 |   zoom_in: string
187 |   /**
188 |    * Z​o​o​m​ ​o​u​t
189 |    */
190 |   zoom_out: string
191 | }
192 | 
193 | export type TranslationFunctions = {
194 |   /**
195 |    * About
196 |    */
197 |   about: () => LocalizedString
198 |   /**
199 |    * Advanced
200 |    */
201 |   advanced: () => LocalizedString
202 |   /**
203 |    * Basic
204 |    */
205 |   basic: () => LocalizedString
206 |   /**
207 |    * Cancel
208 |    */
209 |   cancel: () => LocalizedString
210 |   /**
211 |    * Choose a color
212 |    */
213 |   choose_a_color: () => LocalizedString
214 |   /**
215 |    * Code of conduct
216 |    */
217 |   code_of_conduct: () => LocalizedString
218 |   /**
219 |    * Color
220 |    */
221 |   color: () => LocalizedString
222 |   /**
223 |    * Connect
224 |    */
225 |   connect: () => LocalizedString
226 |   /**
227 |    * Connection established
228 |    */
229 |   connection_established: () => LocalizedString
230 |   /**
231 |    * Copy my connection string
232 |    */
233 |   copy_my_connection_string: () => LocalizedString
234 |   /**
235 |    * Disconnect
236 |    */
237 |   disconnect: () => LocalizedString
238 |   /**
239 |    * Fullscreen
240 |    */
241 |   fullscreen: () => LocalizedString
242 |   /**
243 |    * Host a session
244 |    */
245 |   host_a_session: () => LocalizedString
246 |   /**
247 |    * Host connection string
248 |    */
249 |   host_connection_string: () => LocalizedString
250 |   /**
251 |    * Hosting a session
252 |    */
253 |   hosting_a_session: () => LocalizedString
254 |   /**
255 |    * Is microphone active on connect
256 |    */
257 |   is_microphone_active_on_connect: () => LocalizedString
258 |   /**
259 |    * Join a session
260 |    */
261 |   join_a_session: () => LocalizedString
262 |   /**
263 |    * Joined a session
264 |    */
265 |   joined_a_session: () => LocalizedString
266 |   /**
267 |    * Language
268 |    */
269 |   language: () => LocalizedString
270 |   /**
271 |    * Choose your preferred language (after changing the language, you need to restart the app)
272 |    */
273 |   language_description: () => LocalizedString
274 |   /**
275 |    * Media
276 |    */
277 |   media: () => LocalizedString
278 |   /**
279 |    * Microphone active
280 |    */
281 |   microphone_active: () => LocalizedString
282 |   /**
283 |    * Microphone inactive
284 |    */
285 |   microphone_inactive: () => LocalizedString
286 |   /**
287 |    * Not streaming your display
288 |    */
289 |   not_streaming_your_display: () => LocalizedString
290 |   /**
291 |    * Participant connection string
292 |    */
293 |   participant_connection_string: () => LocalizedString
294 |   /**
295 |    * Privacy policy
296 |    */
297 |   privacty_policy: () => LocalizedString
298 |   /**
299 |    * Remote cursors disabled
300 |    */
301 |   remote_cursors_disabled: () => LocalizedString
302 |   /**
303 |    * Remote cursors enabled
304 |    */
305 |   remote_cursors_enabled: () => LocalizedString
306 |   /**
307 |    * Remote screen
308 |    */
309 |   remote_screen: () => LocalizedString
310 |   /**
311 |    * Report a bug
312 |    */
313 |   report_a_bug: () => LocalizedString
314 |   /**
315 |    * Save
316 |    */
317 |   save: () => LocalizedString
318 |   /**
319 |    * See the code
320 |    */
321 |   see_the_code: () => LocalizedString
322 |   /**
323 |    * Session started
324 |    */
325 |   session_started: () => LocalizedString
326 |   /**
327 |    * Settings
328 |    */
329 |   settings: () => LocalizedString
330 |   /**
331 |    * Shoulders of giants
332 |    */
333 |   shoulders_of_giants: () => LocalizedString
334 |   /**
335 |    * Bananas Screen Sharing is built on top of the following open-source projects (in no particular order)
336 |    */
337 |   shoulders_of_giants_description: () => LocalizedString
338 |   /**
339 |    * Start a new session
340 |    */
341 |   start_a_new_session: () => LocalizedString
342 |   /**
343 |    * Streaming your display
344 |    */
345 |   streaming_your_display: () => LocalizedString
346 |   /**
347 |    * STUN/TURN Server Objects (separated by new lines)
348 |    */
349 |   stun_turn_server_objects: () => LocalizedString
350 |   /**
351 |    * Terms of service
352 |    */
353 |   terms_of_service: () => LocalizedString
354 |   /**
355 |    * Username
356 |    */
357 |   username: () => LocalizedString
358 |   /**
359 |    * Website
360 |    */
361 |   website: () => LocalizedString
362 |   /**
363 |    * Zoom in
364 |    */
365 |   zoom_in: () => LocalizedString
366 |   /**
367 |    * Zoom out
368 |    */
369 |   zoom_out: () => LocalizedString
370 | }
371 | 
372 | export type Formatters = {}
373 | 


--------------------------------------------------------------------------------
/src/i18n/i18n-util.async.ts:
--------------------------------------------------------------------------------
 1 | // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
 2 | /* eslint-disable */
 3 | 
 4 | import { initFormatters } from './formatters'
 5 | import type { Locales, Translations } from './i18n-types'
 6 | import { loadedFormatters, loadedLocales, locales } from './i18n-util'
 7 | 
 8 | const localeTranslationLoaders = {
 9 |   de: () => import('./de'),
10 |   en: () => import('./en'),
11 |   fr: () => import('./fr'),
12 |   zh: () => import('./zh')
13 | }
14 | 
15 | const updateDictionary = (locale: Locales, dictionary: Partial<Translations>): Translations =>
16 |   (loadedLocales[locale] = { ...loadedLocales[locale], ...dictionary })
17 | 
18 | export const importLocaleAsync = async (locale: Locales): Promise<Translations> =>
19 |   (await localeTranslationLoaders[locale]()).default as unknown as Translations
20 | 
21 | export const loadLocaleAsync = async (locale: Locales): Promise<void> => {
22 |   updateDictionary(locale, await importLocaleAsync(locale))
23 |   loadFormatters(locale)
24 | }
25 | 
26 | export const loadAllLocalesAsync = (): Promise<void[]> => Promise.all(locales.map(loadLocaleAsync))
27 | 
28 | export const loadFormatters = (locale: Locales): void =>
29 |   void (loadedFormatters[locale] = initFormatters(locale))
30 | 


--------------------------------------------------------------------------------
/src/i18n/i18n-util.sync.ts:
--------------------------------------------------------------------------------
 1 | // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
 2 | /* eslint-disable */
 3 | 
 4 | import { initFormatters } from './formatters'
 5 | import type { Locales, Translations } from './i18n-types'
 6 | import { loadedFormatters, loadedLocales, locales } from './i18n-util'
 7 | 
 8 | import de from './de'
 9 | import en from './en'
10 | import fr from './fr'
11 | import zh from './zh'
12 | 
13 | const localeTranslations = {
14 |   de,
15 |   en,
16 |   fr,
17 |   zh
18 | }
19 | 
20 | export const loadLocale = (locale: Locales): void => {
21 |   if (loadedLocales[locale]) return
22 | 
23 |   loadedLocales[locale] = localeTranslations[locale] as unknown as Translations
24 |   loadFormatters(locale)
25 | }
26 | 
27 | export const loadAllLocales = (): void => locales.forEach(loadLocale)
28 | 
29 | export const loadFormatters = (locale: Locales): void =>
30 |   void (loadedFormatters[locale] = initFormatters(locale))
31 | 


--------------------------------------------------------------------------------
/src/i18n/i18n-util.ts:
--------------------------------------------------------------------------------
 1 | // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
 2 | /* eslint-disable */
 3 | 
 4 | import {
 5 |   i18n as initI18n,
 6 |   i18nObject as initI18nObject,
 7 |   i18nString as initI18nString
 8 | } from 'typesafe-i18n'
 9 | import type { LocaleDetector } from 'typesafe-i18n/detectors'
10 | import type { LocaleTranslationFunctions, TranslateByString } from 'typesafe-i18n'
11 | import { detectLocale as detectLocaleFn } from 'typesafe-i18n/detectors'
12 | import { initExtendDictionary } from 'typesafe-i18n/utils'
13 | import type { Formatters, Locales, Translations, TranslationFunctions } from './i18n-types'
14 | 
15 | export const baseLocale: Locales = 'en'
16 | 
17 | export const locales: Locales[] = ['de', 'en', 'fr', 'zh']
18 | 
19 | export const isLocale = (locale: string): locale is Locales => locales.includes(locale as Locales)
20 | 
21 | export const loadedLocales: Record<Locales, Translations> = {} as Record<Locales, Translations>
22 | 
23 | export const loadedFormatters: Record<Locales, Formatters> = {} as Record<Locales, Formatters>
24 | 
25 | export const extendDictionary = initExtendDictionary<Translations>()
26 | 
27 | export const i18nString = (locale: Locales): TranslateByString =>
28 |   initI18nString<Locales, Formatters>(locale, loadedFormatters[locale])
29 | 
30 | export const i18nObject = (locale: Locales): TranslationFunctions =>
31 |   initI18nObject<Locales, Translations, TranslationFunctions, Formatters>(
32 |     locale,
33 |     loadedLocales[locale],
34 |     loadedFormatters[locale]
35 |   )
36 | 
37 | export const i18n = (): LocaleTranslationFunctions<Locales, Translations, TranslationFunctions> =>
38 |   initI18n<Locales, Translations, TranslationFunctions, Formatters>(loadedLocales, loadedFormatters)
39 | 
40 | export const detectLocale = (...detectors: LocaleDetector[]): Locales =>
41 |   detectLocaleFn<Locales>(baseLocale, locales, ...detectors)
42 | 


--------------------------------------------------------------------------------
/src/i18n/zh/index.ts:
--------------------------------------------------------------------------------
 1 | import type { Translation } from '../i18n-types'
 2 | 
 3 | const zh = {
 4 |   about: '关于',
 5 |   advanced: '高级',
 6 |   basic: '基本',
 7 |   cancel: '取消',
 8 |   choose_a_color: '选择一个颜色',
 9 |   code_of_conduct: '行为准则',
10 |   color: '颜色',
11 |   connect: '连接',
12 |   connection_established: '连接建立成功',
13 |   copy_my_connection_string: '复制我的连接码',
14 |   disconnect: '连接失败',
15 |   fullscreen: '全屏',
16 |   host_a_session: '发起一个屏幕共享',
17 |   host_connection_string: '创建此共享的邀请码',
18 |   hosting_a_session: '正在屏幕共享',
19 |   is_microphone_active_on_connect: '连接时麦克风是否打开',
20 |   join_a_session: '加入一个屏幕共享',
21 |   joined_a_session: '已加入屏幕共享',
22 |   language: '语言',
23 |   language_description: '选择您的首选语言(更改语言后,您需要重新启动应用程序)',
24 |   media: '媒体',
25 |   microphone_active: '打开麦克风',
26 |   microphone_inactive: '关闭麦克风',
27 |   not_streaming_your_display: '不要流失传输您的显示器',
28 |   participant_connection_string: '参与者的连接码',
29 |   privacty_policy: '隐私政策',
30 |   remote_cursors_disabled: '运程光标已关闭',
31 |   remote_cursors_enabled: '远程光标已启用',
32 |   remote_screen: '远程屏幕',
33 |   report_a_bug: '报告一个错误',
34 |   save: '保存',
35 |   see_the_code: '查看代码',
36 |   session_started: '共享已开始',
37 |   settings: '设置',
38 |   shoulders_of_giants: '巨人的肩膀',
39 |   shoulders_of_giants_description:
40 |     'Bananas Screen Sharing是建立在以下开源项目之上的(没有特定顺序)',
41 |   start_a_new_session: '开始一个新共享',
42 |   streaming_your_display: '流式传输您的显示器',
43 |   stun_turn_server_objects: 'STUN/TURN服务器对象(用新行分隔)',
44 |   terms_of_service: '服务条款',
45 |   username: '用户名',
46 |   website: '网站',
47 |   zoom_in: '放大',
48 |   zoom_out: '缩小'
49 | } satisfies Translation
50 | 
51 | export default zh
52 | 


--------------------------------------------------------------------------------
/src/main/cursors.ts:
--------------------------------------------------------------------------------
 1 | import { BrowserWindow, screen } from 'electron'
 2 | import { loadWindowContents } from './utils'
 3 | import { join } from 'path'
 4 | 
 5 | export const createCursorsWindow = async (): Promise<BrowserWindow> => {
 6 |   const dimensions = screen.getPrimaryDisplay().workAreaSize
 7 |   const win = new BrowserWindow({
 8 |     width: dimensions.width,
 9 |     height: dimensions.height,
10 |     x: 0,
11 |     y: 0,
12 |     show: true,
13 |     frame: false,
14 |     autoHideMenuBar: true,
15 |     transparent: true,
16 |     closable: true,
17 |     fullscreen: true,
18 |     minimizable: false,
19 |     webPreferences: {
20 |       preload: join(__dirname, '../preload/cursors.js'),
21 |       sandbox: false,
22 |       contextIsolation: true,
23 |       nodeIntegration: true
24 |     }
25 |   })
26 |   loadWindowContents(win, 'cursors.html')
27 |   win.setIgnoreMouseEvents(true)
28 |   win.setAlwaysOnTop(true, 'normal', 1)
29 |   win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true })
30 |   return win
31 | }
32 | 


--------------------------------------------------------------------------------
/src/main/index.ts:
--------------------------------------------------------------------------------
  1 | import { app, shell, BrowserWindow, session, desktopCapturer } from 'electron'
  2 | import path from 'path'
  3 | import { join } from 'path'
  4 | import { electronApp, optimizer, is } from '@electron-toolkit/utils'
  5 | import icon from '../../resources/icon.png?asset'
  6 | import { windowStateKeeper } from './stateKeeper'
  7 | import { ipcMainHandlersInit } from './ipcMainHandlers'
  8 | import { isInProductionMode } from './utils'
  9 | 
 10 | const CUSTOM_PROTOCOL = 'bananas'
 11 | 
 12 | let MAIN_WINDOW: BrowserWindow
 13 | 
 14 | if (process.defaultApp) {
 15 |   if (process.argv.length >= 2) {
 16 |     app.setAsDefaultProtocolClient(CUSTOM_PROTOCOL, process.execPath, [
 17 |       path.resolve(process.argv[1])
 18 |     ])
 19 |   }
 20 | } else {
 21 |   app.setAsDefaultProtocolClient(CUSTOM_PROTOCOL)
 22 | }
 23 | 
 24 | if (isInProductionMode()) {
 25 |   const SINGLE_INSTANCE_LOCK = app.requestSingleInstanceLock()
 26 | 
 27 |   if (!SINGLE_INSTANCE_LOCK) {
 28 |     app.quit()
 29 |   }
 30 | }
 31 | 
 32 | const sendOpenBananasUrlToRenderer = (url: string): void => {
 33 |   MAIN_WINDOW.webContents.send('openBananasURL', url)
 34 | }
 35 | 
 36 | app.on('second-instance', (_, commandLine) => {
 37 |   if (MAIN_WINDOW) {
 38 |     if (MAIN_WINDOW.isMinimized()) MAIN_WINDOW.restore()
 39 |     MAIN_WINDOW.focus()
 40 |   }
 41 |   const url = commandLine.pop()
 42 |   if (url) sendOpenBananasUrlToRenderer(url)
 43 | })
 44 | 
 45 | app.on('open-url', (evt, url: string) => {
 46 |   evt.preventDefault()
 47 |   sendOpenBananasUrlToRenderer(url)
 48 | })
 49 | 
 50 | async function createWindow(): Promise<void> {
 51 |   const mainWindowState = await windowStateKeeper('main')
 52 | 
 53 |   MAIN_WINDOW = new BrowserWindow({
 54 |     width: mainWindowState.width,
 55 |     height: mainWindowState.height,
 56 |     minWidth: 400,
 57 |     minHeight: 200,
 58 |     x: mainWindowState.x,
 59 |     y: mainWindowState.y,
 60 |     show: false,
 61 |     autoHideMenuBar: true,
 62 |     ...(process.platform === 'linux' ? { icon } : {}),
 63 |     webPreferences: {
 64 |       preload: join(__dirname, '../preload/index.js'),
 65 |       sandbox: false,
 66 |       contextIsolation: true,
 67 |       nodeIntegration: true
 68 |     }
 69 |   })
 70 | 
 71 |   mainWindowState.track(MAIN_WINDOW)
 72 | 
 73 |   session.defaultSession.setDisplayMediaRequestHandler((_, callback) => {
 74 |     desktopCapturer.getSources({ types: ['screen'] }).then((sources) => {
 75 |       callback({ video: sources[0] })
 76 |     })
 77 |   })
 78 | 
 79 |   MAIN_WINDOW.on('ready-to-show', () => {
 80 |     MAIN_WINDOW.show()
 81 |   })
 82 | 
 83 |   MAIN_WINDOW.webContents.setWindowOpenHandler((details) => {
 84 |     shell.openExternal(details.url)
 85 |     return { action: 'deny' }
 86 |   })
 87 | 
 88 |   if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
 89 |     MAIN_WINDOW.loadURL(process.env['ELECTRON_RENDERER_URL'])
 90 |   } else {
 91 |     MAIN_WINDOW.loadFile(join(__dirname, '../renderer/index.html'))
 92 |   }
 93 | 
 94 |   if (mainWindowState.isMaximized) {
 95 |     MAIN_WINDOW.maximize()
 96 |   }
 97 | }
 98 | 
 99 | app.whenReady().then(async () => {
100 |   electronApp.setAppUserModelId('net.getbananas')
101 | 
102 |   app.on('browser-window-created', (_, window) => {
103 |     optimizer.watchWindowShortcuts(window)
104 |   })
105 | 
106 |   ipcMainHandlersInit()
107 | 
108 |   await createWindow()
109 |   const coldStartUrl = process.argv.find((arg) => arg.startsWith(CUSTOM_PROTOCOL + '://'))
110 |   if (coldStartUrl) {
111 |     sendOpenBananasUrlToRenderer(coldStartUrl)
112 |   }
113 | 
114 |   app.on('activate', function () {
115 |     if (BrowserWindow.getAllWindows().length === 0) createWindow()
116 |   })
117 | })
118 | 
119 | app.on('window-all-closed', () => {
120 |   if (process.platform !== 'darwin') {
121 |     app.quit()
122 |   }
123 | })
124 | 


--------------------------------------------------------------------------------
/src/main/ipcMainHandlers.ts:
--------------------------------------------------------------------------------
 1 | import type { SettingsData } from './stateKeeper'
 2 | import { app, BrowserWindow, ipcMain, screen } from 'electron'
 3 | import { createCursorsWindow } from './cursors'
 4 | import { settingsKeeper } from './stateKeeper'
 5 | 
 6 | export const ipcMainHandlersInit = (): void => {
 7 |   const availableDimensions = screen.getPrimaryDisplay().workAreaSize
 8 |   let remoteCursorsWindow: BrowserWindow | null = null
 9 |   let remoteCursorsActive = false
10 | 
11 |   ipcMain.handle('toggleRemoteCursors', async (_, state) => {
12 |     remoteCursorsActive = state
13 |     if (!remoteCursorsWindow && remoteCursorsActive) {
14 |       remoteCursorsWindow = await createCursorsWindow()
15 |       remoteCursorsWindow.on('closed', () => {
16 |         remoteCursorsWindow = null
17 |         remoteCursorsActive = false
18 |       })
19 |       return
20 |     }
21 |     if (remoteCursorsWindow && !remoteCursorsActive) {
22 |       remoteCursorsWindow.close()
23 |       remoteCursorsWindow = null
24 |       return
25 |     }
26 |     console.error('Invalid state')
27 |   })
28 |   ipcMain.handle('updateRemoteCursor', async (_, state): Promise<void> => {
29 |     if (!remoteCursorsActive) return
30 |     if (!remoteCursorsWindow) return
31 |     const realX: string = (state.x * availableDimensions.width).toString()
32 |     const realY: string = (state.y * availableDimensions.height).toString()
33 |     const x = parseInt(realX, 10)
34 |     const y = parseInt(realY, 10)
35 |     const data = {
36 |       ...state,
37 |       x,
38 |       y
39 |     }
40 |     remoteCursorsWindow.webContents.send('updateRemoteCursor', data)
41 |   })
42 |   ipcMain.handle('remoteCursorPing', async (_, cursorId): Promise<void> => {
43 |     if (!remoteCursorsActive) return
44 |     if (!remoteCursorsWindow) return
45 |     remoteCursorsWindow.webContents.send('remoteCursorPing', cursorId)
46 |   })
47 |   ipcMain.handle('updateSettings', async (_, settings): Promise<void> => {
48 |     const settingsKeeperInstance = await settingsKeeper()
49 |     settingsKeeperInstance.set(settings)
50 |   })
51 |   ipcMain.handle('getSettings', async (): Promise<SettingsData> => {
52 |     const settingsKeeperInstance = await settingsKeeper()
53 |     return settingsKeeperInstance.get()
54 |   })
55 |   ipcMain.handle('getAppVersion', (): string => {
56 |     return app.getVersion()
57 |   })
58 | }
59 | 


--------------------------------------------------------------------------------
/src/main/stateKeeper.ts:
--------------------------------------------------------------------------------
  1 | import { screen } from 'electron'
  2 | import settings from 'electron-settings'
  3 | import { debounce } from './utils'
  4 | 
  5 | type IceServer = {
  6 |   urls: string
  7 |   username?: string
  8 |   credential?: string
  9 | }
 10 | 
 11 | export type SettingsData = {
 12 |   username: string
 13 |   color: string
 14 |   language: string
 15 |   isMicrophoneEnabledOnConnect: boolean
 16 |   iceServers: IceServer[]
 17 | }
 18 | 
 19 | type Settings = {
 20 |   get: () => SettingsData
 21 |   set: (data: SettingsData) => void
 22 | }
 23 | 
 24 | type WindowState = {
 25 |   x?: number
 26 |   y?: number
 27 |   width: number
 28 |   height: number
 29 |   isMaximized: boolean
 30 | }
 31 | 
 32 | type WindowStateKeeper = WindowState & {
 33 |   track: (win: Electron.BrowserWindow) => void
 34 | }
 35 | 
 36 | export const settingsKeeper = async (): Promise<Settings> => {
 37 |   const defaultSettings: SettingsData = {
 38 |     username: 'Banana Joe',
 39 |     color: '#ffffff',
 40 |     language: 'en',
 41 |     isMicrophoneEnabledOnConnect: true,
 42 |     iceServers: [
 43 |       {
 44 |         urls: 'stun:stun.l.google.com:19302'
 45 |       }
 46 |     ]
 47 |   }
 48 |   const hasSettings = await settings.has('settings')
 49 |   if (hasSettings) {
 50 |     const data = (await settings.get('settings')) as unknown as SettingsData
 51 |     return {
 52 |       get: (): SettingsData => {
 53 |         return {
 54 |           ...defaultSettings,
 55 |           ...data
 56 |         }
 57 |       },
 58 |       set: (data: SettingsData) => settings.set('settings', data)
 59 |     }
 60 |   }
 61 |   return {
 62 |     get: (): SettingsData => defaultSettings,
 63 |     set: (data: SettingsData) => settings.set('settings', data)
 64 |   }
 65 | }
 66 | 
 67 | export const windowStateKeeper = async (windowName: string): Promise<WindowStateKeeper> => {
 68 |   let window: Electron.BrowserWindow
 69 |   let windowState: WindowState
 70 | 
 71 |   const setBounds = async (): Promise<WindowState> => {
 72 |     const hasState = await settings.has(`windowState.${windowName}`)
 73 |     if (hasState) {
 74 |       return (await settings.get(`windowState.${windowName}`)) as unknown as WindowState
 75 |     }
 76 | 
 77 |     const size = screen.getPrimaryDisplay().workAreaSize
 78 | 
 79 |     return {
 80 |       width: size.width / 2,
 81 |       height: size.height / 2,
 82 |       isMaximized: false
 83 |     }
 84 |   }
 85 | 
 86 |   const saveState = async (): Promise<void> => {
 87 |     const bounds = window.getBounds()
 88 |     windowState = {
 89 |       ...bounds,
 90 |       isMaximized: window.isMaximized()
 91 |     }
 92 |     await settings.set(`windowState.${windowName}`, windowState)
 93 |   }
 94 | 
 95 |   const track = async (win: Electron.BrowserWindow): Promise<void> => {
 96 |     window = win
 97 |     win.on('move', debounce(saveState, 400))
 98 |     win.on('resize', debounce(saveState, 400))
 99 |     win.on('unmaximize', debounce(saveState, 400))
100 |   }
101 | 
102 |   windowState = await setBounds()
103 | 
104 |   return {
105 |     x: windowState.x,
106 |     y: windowState.y,
107 |     width: windowState.width,
108 |     height: windowState.height,
109 |     isMaximized: windowState.isMaximized,
110 |     track
111 |   }
112 | }
113 | 


--------------------------------------------------------------------------------
/src/main/utils.ts:
--------------------------------------------------------------------------------
 1 | import { is } from '@electron-toolkit/utils'
 2 | import { join } from 'path'
 3 | 
 4 | export const loadWindowContents = (win: Electron.BrowserWindow, file: string): void => {
 5 |   if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
 6 |     win.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/' + file)
 7 |   } else {
 8 |     win.loadFile(join(__dirname, '../renderer/' + file))
 9 |   }
10 | }
11 | 
12 | export const isInProductionMode = (): boolean => {
13 |   if (is.dev && process.env['ELECTRON_RENDERER_URL']) return false
14 |   return true
15 | }
16 | 
17 | export const debounce = <T extends (...args: unknown[]) => void>(
18 |   func: T,
19 |   wait: number
20 | ): ((...args: Parameters<T>) => void) => {
21 |   let timeout: NodeJS.Timeout
22 |   return (...args: Parameters<T>): void => {
23 |     clearTimeout(timeout)
24 |     timeout = setTimeout(() => {
25 |       func(...args)
26 |     }, wait)
27 |   }
28 | }
29 | 


--------------------------------------------------------------------------------
/src/preload/cursor.svg:
--------------------------------------------------------------------------------
1 | <svg fill="#ffffff" stroke="#000000" stroke-width="1" height="24px" width="24px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
2 | 	 y="0px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
3 | <g>
4 | 	<path d="M14.1,15.74l2.77,6.47l-3.68,1.58l-2.76-6.45L6.03,21V2l14,13L14.1,15.74z"/>
5 | </g>
6 | </svg>
7 | 


--------------------------------------------------------------------------------
/src/preload/cursors.ts:
--------------------------------------------------------------------------------
 1 | /// <reference types="vite/client" />
 2 | import { ipcRenderer } from 'electron'
 3 | import cursorSvg from './cursor.svg?raw'
 4 | 
 5 | type RemoteCursorData = {
 6 |   id: string
 7 |   name: string
 8 |   color: string
 9 |   x: number
10 |   y: number
11 | }
12 | 
13 | type RemoteCursor = {
14 |   ping: () => void
15 |   getId: () => string
16 |   getRootEl: () => HTMLDivElement
17 |   getCursorEl: () => HTMLOrSVGElement
18 |   getNameEl: () => HTMLSpanElement
19 |   getName: () => string
20 |   getData: () => RemoteCursorData
21 |   update: (data: RemoteCursorData) => void
22 | }
23 | 
24 | const remoteCursors: RemoteCursor[] = []
25 | 
26 | const domParser = new DOMParser()
27 | 
28 | class Cursor {
29 |   private root = document.createElement('div')
30 |   private cursorEl = document.createElement('svg')
31 |   private pingEl = document.createElement('div')
32 |   private nameEl = document.createElement('span')
33 |   private data: RemoteCursorData
34 |   constructor(data: RemoteCursorData) {
35 |     this.data = data
36 |     this.cursorEl = domParser.parseFromString(cursorSvg, 'image/svg+xml').documentElement
37 |     this.root.classList.add('cursor')
38 |     this.pingEl.classList.add('ping', 'is-hidden')
39 |     this.nameEl.classList.add('name')
40 |     this.pingEl.style.setProperty('--ping-color', data.color)
41 |     this.cursorEl.style.setProperty('--cursor-color', data.color)
42 |     this.nameEl.style.setProperty('--name-color', data.color)
43 |     this.nameEl.innerText = data.name
44 |     this.root.id = data.id
45 |     this.root.appendChild(this.pingEl)
46 |     this.root.appendChild(this.cursorEl)
47 |     this.root.appendChild(this.nameEl)
48 |   }
49 |   ping = (): void => {
50 |     this.pingEl.classList.toggle('is-hidden')
51 |     setTimeout(() => {
52 |       this.pingEl.classList.toggle('is-hidden')
53 |     }, 1000)
54 |   }
55 |   getId = (): string => this.data.id
56 |   getRootEl = (): HTMLDivElement => this.root
57 |   getCursorEl = (): HTMLOrSVGElement => this.cursorEl
58 |   getNameEl = (): HTMLSpanElement => this.nameEl
59 |   getName = (): string => this.data.name
60 |   getData = (): RemoteCursorData => this.data
61 |   update = (data: RemoteCursorData): void => {
62 |     this.data = data
63 |     this.root.style.left = `${data.x - 24}px`
64 |     this.root.style.top = `${data.y}px`
65 |   }
66 | }
67 | 
68 | ipcRenderer.on('updateRemoteCursor', (_, data) => {
69 |   let cursor: RemoteCursor | undefined = remoteCursors.find((c) => c.getId() === data.id)
70 |   if (cursor) {
71 |     cursor.update(data)
72 |   } else {
73 |     cursor = new Cursor(data)
74 |     remoteCursors.push(cursor)
75 |     document.body.appendChild(cursor.getRootEl())
76 |   }
77 | })
78 | 
79 | ipcRenderer.on('remoteCursorPing', (_, cursorId) => {
80 |   remoteCursors.find((c) => c.getId() === cursorId)?.ping()
81 | })
82 | 


--------------------------------------------------------------------------------
/src/preload/index.d.ts:
--------------------------------------------------------------------------------
 1 | import { ElectronAPI } from '@electron-toolkit/preload'
 2 | 
 3 | type IceServer = {
 4 |   urls: string
 5 |   username?: string
 6 |   credential?: string
 7 | }
 8 | 
 9 | declare global {
10 |   interface Window {
11 |     electron: ElectronAPI
12 |     BananasApi: {
13 |       toggleRemoteCursors: (state: boolean) => Promise<void>
14 |       remoteCursorPing: (cursorId: string) => Promise<void>
15 |       updateRemoteCursor: (state: {
16 |         id: string
17 |         name: string
18 |         color: string
19 |         x: number
20 |         y: number
21 |       }) => Promise<void>
22 |       updateSettings: (settings: {
23 |         username: string
24 |         color: string
25 |         isMicrophoneEnabledOnConnect: boolean
26 |         iceServers: IceServer[]
27 |       }) => Promise<void>
28 |       getSettings: () => Promise<{
29 |         username: string
30 |         color: string
31 |         isMicrophoneEnabledOnConnect: boolean
32 |         iceServers: IceServer[]
33 |       }>
34 |       getAppVersion: () => Promise<string>
35 |     }
36 |   }
37 | }
38 | 


--------------------------------------------------------------------------------
/src/preload/index.ts:
--------------------------------------------------------------------------------
 1 | import { ipcRenderer } from 'electron'
 2 | import { contextBridge } from 'electron'
 3 | import { electronAPI } from '@electron-toolkit/preload'
 4 | 
 5 | let HANDLE_URL_CLICKS = true
 6 | 
 7 | const onDocumentReady = (callback: () => void): void => {
 8 |   if (document.readyState !== 'complete') {
 9 |     document.addEventListener('DOMContentLoaded', callback)
10 |   } else {
11 |     callback()
12 |   }
13 | }
14 | 
15 | ipcRenderer.on('openBananasURL', (_, url) => {
16 |   if (!HANDLE_URL_CLICKS) return
17 |   onDocumentReady(() => {
18 |     window.postMessage({ type: 'openBananasURL', url }, '*')
19 |   })
20 | })
21 | 
22 | type IceServer = {
23 |   urls: string
24 |   username?: string
25 |   credential?: string
26 | }
27 | 
28 | const BananasApi = {
29 |   getAppVersion: async (): Promise<string> => {
30 |     return await ipcRenderer.invoke('getAppVersion')
31 |   },
32 |   handleUrlClicks: (state: boolean | undefined): boolean => {
33 |     if (state) HANDLE_URL_CLICKS = state
34 |     return HANDLE_URL_CLICKS
35 |   },
36 |   getSettings: async (): Promise<{
37 |     username: string
38 |     color: string
39 |     isMicrophoneEnabledOnConnect: boolean
40 |     iceServers: IceServer[]
41 |   }> => {
42 |     return await ipcRenderer.invoke('getSettings')
43 |   },
44 |   updateSettings: async (settings: {
45 |     username: string
46 |     color: string
47 |     isMicrophoneEnabledOnConnect: boolean
48 |     iceServers: IceServer[]
49 |   }): Promise<void> => {
50 |     ipcRenderer.invoke('updateSettings', settings)
51 |   },
52 |   toggleRemoteCursors: async (state: boolean): Promise<void> => {
53 |     ipcRenderer.invoke('toggleRemoteCursors', state)
54 |   },
55 |   remoteCursorPing: async (cursorId: string): Promise<void> => {
56 |     ipcRenderer.invoke('remoteCursorPing', cursorId)
57 |   },
58 |   updateRemoteCursor: async (state: {
59 |     id: string
60 |     name: string
61 |     color: string
62 |     x: number
63 |     y: number
64 |   }): Promise<void> => {
65 |     ipcRenderer.invoke('updateRemoteCursor', state)
66 |   }
67 | }
68 | 
69 | try {
70 |   contextBridge.exposeInMainWorld('electron', electronAPI)
71 |   contextBridge.exposeInMainWorld('BananasApi', BananasApi)
72 | } catch (error) {
73 |   console.error(error)
74 | }
75 | 


--------------------------------------------------------------------------------
/src/renderer/cursors.html:
--------------------------------------------------------------------------------
 1 | <!doctype html>
 2 | <html lang="en">
 3 |   <head>
 4 |     <meta charset="UTF-8" />
 5 |     <title>Bananas: Cursors</title>
 6 |     <meta
 7 |       http-equiv="Content-Security-Policy"
 8 |       content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
 9 |     />
10 |     <link rel="stylesheet" href="./src/cursors.css" />
11 |   </head>
12 |   <body></body>
13 | </html>
14 | 


--------------------------------------------------------------------------------
/src/renderer/index.html:
--------------------------------------------------------------------------------
 1 | <!doctype html>
 2 | <html lang="en">
 3 |   <head>
 4 |     <meta charset="UTF-8" />
 5 |     <title>Bananas</title>
 6 |     <meta
 7 |       http-equiv="Content-Security-Policy"
 8 |       content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
 9 |     />
10 |   </head>
11 | 
12 |   <body>
13 |     <div id="app"></div>
14 |     <script type="module" src="/src/main.ts"></script>
15 |   </body>
16 | </html>
17 | 


--------------------------------------------------------------------------------
/src/renderer/src/About.shoulders-of-giants.json:
--------------------------------------------------------------------------------
  1 | [
  2 |   {
  3 |     "title": "Discord",
  4 |     "description": "Discord is a voice, video and text communication service to talk and hang out with your friends and communities.",
  5 |     "usage": "Used for supporting the community and providing help.",
  6 |     "license": null,
  7 |     "url": "https://discord.com/"
  8 |   },
  9 |   {
 10 |     "title": "GitHub",
 11 |     "description": "GitHub is a code hosting platform for version control and collaboration.",
 12 |     "usage": "Used for hosting the source code, building and distributing the releases.",
 13 |     "license": null,
 14 |     "url": "https://github.com/github"
 15 |   },
 16 |   {
 17 |     "title": "Electron",
 18 |     "description": "Build cross-platform desktop apps with JavaScript, HTML, and CSS.",
 19 |     "usage": "Used for building the cross-platform desktop application.",
 20 |     "license": "MIT",
 21 |     "url": "https://github.com/electron/electron"
 22 |   },
 23 |   {
 24 |     "title": "Electron Builder",
 25 |     "description": "A complete solution to package and build a ready for distribution Electron app with 'auto update' support out of the box.",
 26 |     "usage": "Used for building the cross-platform desktop application.",
 27 |     "license": "MIT",
 28 |     "url": "https://github.com/electron-userland/electron-builder"
 29 |   },
 30 |   {
 31 |     "title": "Svelte",
 32 |     "description": "Cybernetically enhanced web apps.",
 33 |     "usage": "Used for building the user interface.",
 34 |     "license": "MIT",
 35 |     "url": "https://github.com/sveltejs/svelte"
 36 |   },
 37 |   {
 38 |     "title": "Bulma",
 39 |     "description": "A modern CSS framework based on Flexbox.",
 40 |     "usage": "Used for styling the user interface.",
 41 |     "license": "MIT",
 42 |     "url": "https://github.com/jgthms/bulma"
 43 |   },
 44 |   {
 45 |     "title": "eslint",
 46 |     "description": "Find and fix problems in your JavaScript code.",
 47 |     "usage": "Used for linting the application.",
 48 |     "license": "MIT",
 49 |     "url": "https://github.com/eslint/eslint"
 50 |   },
 51 |   {
 52 |     "title": "prettier",
 53 |     "description": "An opinionated code formatter.",
 54 |     "usage": "Used for formatting the application.",
 55 |     "license": "MIT",
 56 |     "url": "https://github.com/prettier/prettier"
 57 |   },
 58 |   {
 59 |     "title": "Vscode",
 60 |     "description": "Visual Studio Code is a code editor redefined and optimized for building and debugging modern web and cloud applications.",
 61 |     "usage": "Used for writing the application.",
 62 |     "license": "MIT",
 63 |     "url": "https://github.com/microsoft/vscode"
 64 |   },
 65 |   {
 66 |     "title": "Neovim",
 67 |     "description": "Vim-fork focused on extensibility and usability.",
 68 |     "usage": "Used for writing the application.",
 69 |     "license": "Apache-2.0",
 70 |     "url": "https://github.com/neovim/neovim"
 71 |   },
 72 |   {
 73 |     "title": "Vite",
 74 |     "description": "A build tool that aims to provide a faster and leaner development experience for modern web projects.",
 75 |     "usage": "Used for building and developing the application.",
 76 |     "license": "MIT",
 77 |     "url": "https://github.com/vitejs/vite"
 78 |   },
 79 |   {
 80 |     "title": "TypeScript",
 81 |     "description": "TypeScript is a superset of JavaScript that compiles to clean JavaScript output.",
 82 |     "usage": "Used for writing the application in a statically typed language.",
 83 |     "license": "Apache-2.0",
 84 |     "url": "https://github.com/microsoft/TypeScript"
 85 |   },
 86 |   {
 87 |     "title": "Bun",
 88 |     "description": "Bun is an all-in-one toolkit for JavaScript and TypeScript apps.",
 89 |     "usage": "Used for installing and managing dependencies and running scripts.",
 90 |     "license": "MIT",
 91 |     "url": "https://github.com/oven-sh/bun"
 92 |   },
 93 |   {
 94 |     "title": "SweetAlert2",
 95 |     "description": "A beautiful, responsive, customizable, accessible (WAI-ARIA) replacement for JavaScript's popup boxes.",
 96 |     "usage": "Used for displaying alerts and modals.",
 97 |     "license": "MIT",
 98 |     "url": "https://github.com/sweetalert2/sweetalert2"
 99 |   }
100 | ]
101 | 


--------------------------------------------------------------------------------
/src/renderer/src/About.svelte:
--------------------------------------------------------------------------------
  1 | <script lang="ts">
  2 |   import { L } from './translations'
  3 |   import { externalLinkClickHandler } from './Utils'
  4 |   import shoulders from './About.shoulders-of-giants.json'
  5 | 
  6 |   // randomize the order of the shoulders
  7 |   const randomizedShoulders = shoulders.sort(() => Math.random() - 0.5)
  8 | 
  9 |   let version: string
 10 |   ;(async function (): Promise<void> {
 11 |     version = await window.BananasApi.getAppVersion()
 12 |   })()
 13 | 
 14 |   const GITHUB_REPO_URL = 'https://github.com/mistweaverco/bananas'
 15 | 
 16 |   function openExternalURL(e: MouseEvent & { currentTarget: HTMLButtonElement }): void {
 17 |     const url = e.currentTarget.dataset.url
 18 |     if (!url) return
 19 | 
 20 |     externalLinkClickHandler(e.currentTarget, url)
 21 |   }
 22 | </script>
 23 | 
 24 | <div class="container p-5 content">
 25 |   <h1 class="title">{L.about()}</h1>
 26 |   <p>You are using <code>{version}</code> of Bananas Screen Sharing</p>
 27 |   <hr />
 28 | 
 29 |   <button class="button is-secondary" data-url="https://getbananas.net" on:click={openExternalURL}>
 30 |     <span class="icon">
 31 |       <i class="fa-solid fa-globe"></i>
 32 |     </span>
 33 |     <strong>{L.website()}</strong>
 34 |   </button>
 35 |   <button
 36 |     class="button is-secondary"
 37 |     data-url="{GITHUB_REPO_URL}/issues/new"
 38 |     on:click={openExternalURL}
 39 |   >
 40 |     <span class="icon">
 41 |       <i class="fa-solid fa-bug"></i>
 42 |     </span>
 43 |     <strong>{L.report_a_bug()}</strong>
 44 |   </button>
 45 |   <button class="button is-secondary" data-url={GITHUB_REPO_URL} on:click={openExternalURL}>
 46 |     <span class="icon">
 47 |       <i class="fa-solid fa-code"></i>
 48 |     </span>
 49 |     <strong>{L.see_the_code()}</strong>
 50 |   </button>
 51 |   <button
 52 |     class="button is-secondary"
 53 |     data-url="{GITHUB_REPO_URL}/blob/main/PRIVACY.md"
 54 |     on:click={openExternalURL}
 55 |   >
 56 |     <span class="icon">
 57 |       <i class="fa-solid fa-lock"></i>
 58 |     </span>
 59 |     <strong>{L.privacty_policy()}</strong>
 60 |   </button>
 61 |   <button
 62 |     class="button is-secondary"
 63 |     data-url="{GITHUB_REPO_URL}/blob/main/TOS.md"
 64 |     on:click={openExternalURL}
 65 |   >
 66 |     <span class="icon">
 67 |       <i class="fa-solid fa-book"></i>
 68 |     </span>
 69 |     <strong>{L.terms_of_service()}</strong>
 70 |   </button>
 71 |   <button
 72 |     class="button is-secondary"
 73 |     data-url="{GITHUB_REPO_URL}/blob/main/CODE_OF_CONDUCT.md"
 74 |     on:click={openExternalURL}
 75 |   >
 76 |     <span class="icon">
 77 |       <i class="fa-solid fa-heart"></i>
 78 |     </span>
 79 |     <strong>{L.code_of_conduct()}</strong>
 80 |   </button>
 81 |   <hr />
 82 |   <h2 class="title is-4">{L.shoulders_of_giants()}</h2>
 83 |   <p>
 84 |     {L.shoulders_of_giants_description()}
 85 |   </p>
 86 |   <ul>
 87 |     {#each randomizedShoulders as shoulder}
 88 |       <li>
 89 |         <p>
 90 |           <a href={shoulder.url} target="_blank" rel="noopener">
 91 |             <strong>{shoulder.title}</strong>
 92 |             {shoulder.license ? '- ' + shoulder.license : ''}
 93 |           </a>
 94 |         </p>
 95 |         <p>{shoulder.description}</p>
 96 |         <p>{shoulder.usage}</p>
 97 |         <hr />
 98 |       </li>
 99 |     {/each}
100 |   </ul>
101 | </div>
102 | 


--------------------------------------------------------------------------------
/src/renderer/src/App.svelte:
--------------------------------------------------------------------------------
 1 | <script lang="ts">
 2 |   import Navigation from './Navigation.svelte'
 3 |   import Join from './Join.svelte'
 4 |   import Host from './Host.svelte'
 5 |   import Settings from './Settings.svelte'
 6 |   import About from './About.svelte'
 7 |   import {
 8 |     useActiveView,
 9 |     useNavigationEnabled,
10 |     useIsHosting,
11 |     useIsWatching,
12 |     useParticipantUrl,
13 |     useHostUrl
14 |   } from './stores'
15 |   import { getDataFromBananasUrl } from './Utils'
16 |   const activeView = useActiveView()
17 |   const participantUrl = useParticipantUrl()
18 |   const hostUrl = useHostUrl()
19 |   const isHosting = useIsHosting()
20 |   useNavigationEnabled()
21 |   useIsWatching()
22 |   window.onmessage = async (evt: MessageEvent): Promise<void> => {
23 |     const { data } = evt
24 |     if (data.type !== 'openBananasURL') return
25 |     const urlData = await getDataFromBananasUrl(data.url)
26 |     switch (urlData.type) {
27 |       case 'host':
28 |         $activeView = 'join'
29 |         $participantUrl = data.url
30 |         break
31 |       case 'participant':
32 |         if ($activeView !== 'host' || !$isHosting) return
33 |         $hostUrl = data.url
34 |         break
35 |     }
36 |   }
37 | </script>
38 | 
39 | <Navigation />
40 | 
41 | {#if $activeView === 'join'}
42 |   <Join />
43 | {:else if $activeView === 'host'}
44 |   <Host />
45 | {:else if $activeView === 'settings'}
46 |   <Settings />
47 | {:else if $activeView === 'about'}
48 |   <About />
49 | {/if}
50 | 


--------------------------------------------------------------------------------
/src/renderer/src/AudioVisualizer.svelte:
--------------------------------------------------------------------------------
  1 | <script lang="ts">
  2 |   import { onMount, onDestroy } from 'svelte'
  3 | 
  4 |   export let stream: MediaStream | null = null
  5 |   export let className: string = ''
  6 |   export let visualizerIsActive: boolean = false
  7 | 
  8 |   let canvas: HTMLCanvasElement
  9 |   let audioCtx: AudioContext | null = null
 10 |   let analyser: AnalyserNode | null = null
 11 |   let animationFrameId: number
 12 | 
 13 |   const ACTIVE_THRESHOLD = 0.006 // Threshold for activation, using normalized RMS
 14 |   const ACTIVATION_PERCENTAGE = 0.1
 15 |   const HEIGHT_SCALE = 4
 16 |   const BUFFER_SIZE = 256 // Size of the circular buffer for signal data
 17 |   const DELAY_FRAMES = 50 // Number of frames delay
 18 |   const EASING_FACTOR = 0.1 // Smoothing factor for displayed values
 19 |   const DECAY_RATE = 0.05 // Rate at which bars decay when signal weakens
 20 | 
 21 |   let smoothedRMS = 0
 22 |   let buffer: number[] = [] // Rolling buffer for RMS activation
 23 |   let signalBuffer: number[][] = [] // Circular buffer for signal data
 24 |   let displayedValues: number[] = [] // Smoothed displayed values
 25 |   let signalBufferIndex = 0 // Current write position in signal buffer
 26 | 
 27 |   function visualize(s: MediaStream): void {
 28 |     if (!canvas) return
 29 | 
 30 |     if (!audioCtx) {
 31 |       audioCtx = new AudioContext()
 32 |     }
 33 | 
 34 |     const canvasCtx = canvas.getContext('2d')
 35 |     const source = audioCtx.createMediaStreamSource(s)
 36 | 
 37 |     analyser = audioCtx.createAnalyser()
 38 |     analyser.fftSize = 2048
 39 |     const bufferLength = analyser.frequencyBinCount
 40 |     const dataArray = new Uint8Array(bufferLength)
 41 | 
 42 |     // Initialize the signal buffer
 43 |     signalBuffer = Array.from({ length: DELAY_FRAMES }, () => new Array(bufferLength).fill(0))
 44 |     displayedValues = new Array(bufferLength).fill(0) // Initialize displayed values
 45 | 
 46 |     source.connect(analyser)
 47 | 
 48 |     function calculateRMS(data: Uint8Array): number {
 49 |       const squaredSum = data.reduce((sum, value) => sum + Math.pow(value / 128.0 - 1.0, 2), 0)
 50 |       return Math.sqrt(squaredSum / data.length)
 51 |     }
 52 | 
 53 |     function draw(): void {
 54 |       animationFrameId = requestAnimationFrame(draw)
 55 | 
 56 |       if (!canvas || !canvasCtx || !analyser) return
 57 | 
 58 |       const canvasWidth = canvas.width
 59 |       const canvasHeight = canvas.height
 60 |       const centerY = canvasHeight / 2
 61 | 
 62 |       analyser.getByteTimeDomainData(dataArray)
 63 | 
 64 |       // Store the current signal in the circular buffer
 65 |       signalBuffer[signalBufferIndex] = [...dataArray]
 66 |       signalBufferIndex = (signalBufferIndex + 1) % DELAY_FRAMES
 67 | 
 68 |       // Retrieve the delayed signal
 69 |       const delayedSignal = signalBuffer[(signalBufferIndex + 1) % DELAY_FRAMES]
 70 | 
 71 |       // Smoothly update displayed values
 72 |       for (let i = 0; i < delayedSignal.length; i++) {
 73 |         const targetValue = (delayedSignal[i] / 128.0 - 1.0) * 3 // Amplify signal, for better visualization
 74 |         if (Math.abs(targetValue) > Math.abs(displayedValues[i])) {
 75 |           // Easing up to the target value
 76 |           displayedValues[i] += (targetValue - displayedValues[i]) * EASING_FACTOR
 77 |         } else {
 78 |           // Decay toward zero when the signal weakens
 79 |           displayedValues[i] *= 1 - DECAY_RATE
 80 |         }
 81 |         displayedValues[i] = Math.min(Math.max(displayedValues[i], -1), 1) // Clamp to visible range
 82 |       }
 83 | 
 84 |       canvasCtx.clearRect(0, 0, canvasWidth, canvasHeight)
 85 |       canvasCtx.fillStyle = 'transparent'
 86 |       canvasCtx.fillRect(0, 0, canvasWidth, canvasHeight)
 87 | 
 88 |       canvasCtx.lineWidth = 2
 89 |       canvasCtx.strokeStyle = 'rgb(0, 0, 0)'
 90 |       canvasCtx.beginPath()
 91 | 
 92 |       const sliceWidth = canvasWidth / bufferLength
 93 |       let x = 0
 94 | 
 95 |       for (let i = 0; i < bufferLength; i++) {
 96 |         const y = centerY + displayedValues[i] * (centerY * HEIGHT_SCALE * 1.5) // Additional scaling
 97 | 
 98 |         if (i === 0) {
 99 |           canvasCtx.moveTo(x, y)
100 |         } else {
101 |           canvasCtx.lineTo(x, y)
102 |         }
103 |         x += sliceWidth
104 |       }
105 | 
106 |       canvasCtx.lineTo(canvasWidth, centerY)
107 |       canvasCtx.stroke()
108 | 
109 |       // Calculate RMS
110 |       const rms = calculateRMS(dataArray)
111 | 
112 |       // Smooth RMS
113 |       smoothedRMS = 0.8 * smoothedRMS + 0.2 * rms
114 | 
115 |       // Update rolling buffer for activation
116 |       buffer.push(smoothedRMS)
117 |       if (buffer.length > BUFFER_SIZE) {
118 |         buffer.shift()
119 |       }
120 | 
121 |       // Analyze buffer to determine activation
122 |       const activeCount = buffer.filter((value) => value > ACTIVE_THRESHOLD).length
123 |       const activePercentage = activeCount / buffer.length
124 | 
125 |       visualizerIsActive = activePercentage >= ACTIVATION_PERCENTAGE
126 |     }
127 | 
128 |     draw()
129 |   }
130 | 
131 |   onMount(() => {
132 |     if (stream) {
133 |       visualize(stream)
134 |     }
135 |   })
136 | 
137 |   onDestroy(() => {
138 |     if (animationFrameId) {
139 |       cancelAnimationFrame(animationFrameId)
140 |     }
141 |     if (audioCtx) {
142 |       audioCtx.close()
143 |     }
144 |     audioCtx = null
145 |     analyser = null
146 |   })
147 | </script>
148 | 
149 | <canvas class={className} bind:this={canvas}></canvas>
150 | 
151 | <style>
152 |   canvas {
153 |     margin-inline-end: initial important;
154 |     margin-inline-start: initial !important;
155 |   }
156 | </style>
157 | 


--------------------------------------------------------------------------------
/src/renderer/src/BananasTypes.ts:
--------------------------------------------------------------------------------
 1 | export type BananasRemoteCursorData = {
 2 |   id: string
 3 |   name: string
 4 |   color: string
 5 |   x: number
 6 |   y: number
 7 | }
 8 | 
 9 | type IceServer = {
10 |   urls: string
11 |   username?: string
12 |   credential?: string
13 | }
14 | 
15 | export type SettingsData = {
16 |   username: string
17 |   color: string
18 |   isMicrophoneEnabledOnConnect: boolean
19 |   iceServers: IceServer[]
20 | }
21 | 


--------------------------------------------------------------------------------
/src/renderer/src/Config.ts:
--------------------------------------------------------------------------------
 1 | type IceServer = {
 2 |   urls: string
 3 |   username?: string
 4 |   credential?: string
 5 | }
 6 | 
 7 | export const getRTCPeerConnectionConfig = async (): Promise<RTCConfiguration> => {
 8 |   const settings = await window.BananasApi.getSettings()
 9 |   const iceServers = settings.iceServers.map((server: IceServer) => {
10 |     return {
11 |       urls: server.urls,
12 |       username: server.username,
13 |       credential: server.credential
14 |     }
15 |   })
16 |   return {
17 |     iceServers
18 |   }
19 | }
20 | 


--------------------------------------------------------------------------------
/src/renderer/src/Host.svelte:
--------------------------------------------------------------------------------
  1 | <script lang="ts">
  2 |   import { onMount } from 'svelte'
  3 |   import Swal from 'sweetalert2'
  4 |   import { L } from './translations'
  5 |   import { useNavigationEnabled, useIsHosting, useHostUrl } from './stores'
  6 |   import { mayBeConnectionString, getDataFromBananasUrl, ConnectionType } from './Utils'
  7 |   import AudioVisualizer from './AudioVisualizer.svelte'
  8 |   import WebRTC from './WebRTC.svelte'
  9 | 
 10 |   const navigationEnabled = useNavigationEnabled()
 11 |   const isHosting = useIsHosting()
 12 | 
 13 |   let webRTCComponent: WebRTC
 14 |   let connectButton: HTMLButtonElement
 15 |   let copyButton: HTMLButtonElement
 16 | 
 17 |   let connectionState = 'disconnected'
 18 |   let cursorsActive = false
 19 |   let displayStreamActive = false
 20 |   let microphoneActive = false
 21 |   let isStreaming = false
 22 |   let sessionStarted = false
 23 |   let connectionStringIsValid: boolean | null = null
 24 |   let connectToUserName = ''
 25 |   let copyButtonIsLoading = false
 26 |   let connectionString = useHostUrl()
 27 |   let hasAudioInput = false
 28 |   let visualizerIsActive: boolean = true
 29 | 
 30 |   const onConnectionStringChange = async (): Promise<void> => {
 31 |     if ($connectionString === '') {
 32 |       connectionStringIsValid = null
 33 |       return
 34 |     }
 35 |     connectionStringIsValid = mayBeConnectionString(ConnectionType.PARTICIPANT, $connectionString)
 36 |     if (connectionStringIsValid) {
 37 |       const banansData = await getDataFromBananasUrl($connectionString)
 38 |       connectToUserName = banansData.data.username
 39 |     }
 40 |   }
 41 | 
 42 |   const onConnectionStateChange = (): void => {
 43 |     switch (connectionState) {
 44 |       case 'connected':
 45 |         Swal.fire({
 46 |           position: 'top-end',
 47 |           icon: 'success',
 48 |           title: 'Connection established',
 49 |           showConfirmButton: false,
 50 |           timer: 1500
 51 |         })
 52 |         break
 53 |       case 'failed':
 54 |         Swal.fire({
 55 |           position: 'top-end',
 56 |           icon: 'error',
 57 |           title: 'Connection failed',
 58 |           showConfirmButton: false,
 59 |           timer: 1500
 60 |         })
 61 |         break
 62 |       case 'closed':
 63 |         Swal.fire({
 64 |           position: 'top-end',
 65 |           icon: 'info',
 66 |           title: 'Connection closed',
 67 |           showConfirmButton: false,
 68 |           timer: 1500
 69 |         })
 70 |         break
 71 |       default:
 72 |         break
 73 |     }
 74 |   }
 75 | 
 76 |   $: $connectionString, onConnectionStringChange()
 77 |   $: connectionState, onConnectionStateChange()
 78 | 
 79 |   const toggleRemoteCursors = (): void => {
 80 |     cursorsActive = !cursorsActive
 81 |     window.BananasApi.toggleRemoteCursors(cursorsActive)
 82 |     webRTCComponent.ToggleRemoteCursors(cursorsActive)
 83 |   }
 84 | 
 85 |   onMount(async () => {
 86 |     const settings = await window.BananasApi.getSettings()
 87 |     microphoneActive = settings.isMicrophoneEnabledOnConnect
 88 |     connectButton.addEventListener('click', async () => {
 89 |       const data = await getDataFromBananasUrl($connectionString)
 90 |       await webRTCComponent.Connect(data.rtcSessionDescription)
 91 |       isStreaming = true
 92 |       displayStreamActive = true
 93 |     })
 94 |     copyButton.addEventListener('click', async () => {
 95 |       copyButtonIsLoading = true
 96 |       const offer = await webRTCComponent.CreateHostUrl({
 97 |         username: settings.username
 98 |       })
 99 |       navigator.clipboard.writeText(offer)
100 |       setTimeout(() => {
101 |         copyButtonIsLoading = false
102 |       }, 400)
103 |     })
104 |   })
105 |   const onStartSessionButtonClick = async (): Promise<void> => {
106 |     await webRTCComponent.Setup()
107 |     sessionStarted = true
108 |     $navigationEnabled = false
109 |     $isHosting = true
110 |     hasAudioInput = webRTCComponent.HasAudioInput()
111 |   }
112 |   const reset = (): void => {
113 |     $connectionString = ''
114 |     cursorsActive = false
115 |     displayStreamActive = false
116 |     microphoneActive = true
117 |     isStreaming = false
118 |     sessionStarted = false
119 |     connectionStringIsValid = null
120 |     copyButtonIsLoading = false
121 |     $navigationEnabled = true
122 |     $isHosting = false
123 |   }
124 |   const onDisconnectClick = async (): Promise<void> => {
125 |     await webRTCComponent.Disconnect()
126 |     reset()
127 |   }
128 |   const onMicrophoneToggle = async (): Promise<void> => {
129 |     microphoneActive = !microphoneActive
130 |     webRTCComponent.ToggleMicrophone()
131 |   }
132 |   const onDisplayStreamToggle = async (): Promise<void> => {
133 |     displayStreamActive = !displayStreamActive
134 |     webRTCComponent.ToggleDisplayStream()
135 |     if (!displayStreamActive) {
136 |       cursorsActive = false
137 |       window.BananasApi.toggleRemoteCursors(cursorsActive)
138 |       webRTCComponent.ToggleRemoteCursors(cursorsActive)
139 |     }
140 |   }
141 | </script>
142 | 
143 | <WebRTC bind:connectionState bind:this={webRTCComponent} />
144 | 
145 | <div class="container p-5">
146 |   <h1 class="title">{!isStreaming ? L.host_a_session() : L.hosting_a_session()}</h1>
147 |   <div class={!isStreaming ? 'is-hidden' : ''}>
148 |     <div class="fixed-grid">
149 |       <div class="grid">
150 |         <div class="cell">
151 |           <button
152 |             title={displayStreamActive ? L.streaming_your_display() : L.streaming_your_display()}
153 |             class="button {displayStreamActive ? 'is-success' : 'is-danger'}"
154 |             on:click={onDisplayStreamToggle}
155 |           >
156 |             <span class="icon">
157 |               <i class="fa-solid fa-display"></i>
158 |             </span>
159 |           </button>
160 |           {#if hasAudioInput}
161 |             <button
162 |               title={microphoneActive ? 'Microphone active' : 'Microphone muted'}
163 |               class="button {microphoneActive ? 'is-success' : 'is-danger'}"
164 |               on:click={onMicrophoneToggle}
165 |             >
166 |               <span class="icon">
167 |                 {#if microphoneActive}
168 |                   <AudioVisualizer
169 |                     className="icon {!visualizerIsActive ? 'is-hidden' : ''}"
170 |                     bind:visualizerIsActive
171 |                     stream={webRTCComponent.GetAudioStream()}
172 |                   />
173 |                   <i class="fas fa-microphone {visualizerIsActive ? 'is-hidden' : ''}"></i>
174 |                 {:else}
175 |                   <i class="fas fa-microphone-slash"></i>
176 |                 {/if}
177 |               </span>
178 |             </button>
179 |           {/if}
180 |           <button
181 |             title={cursorsActive ? L.remote_cursors_enabled() : L.remote_cursors_disabled()}
182 |             class="button {cursorsActive ? 'is-success' : 'is-danger'} {!displayStreamActive
183 |               ? 'is-hidden'
184 |               : ''}"
185 |             on:click={toggleRemoteCursors}
186 |           >
187 |             <span class="icon">
188 |               <i class="fas fa-mouse-pointer"></i>
189 |             </span>
190 |           </button>
191 |         </div>
192 |         <div class="cell has-text-right">
193 |           <button class="button is-danger" on:click={onDisconnectClick}>
194 |             <span class="icon">
195 |               <i class="fas fa-unlink"></i>
196 |             </span>
197 |             <span>{L.disconnect()}</span>
198 |           </button>
199 |         </div>
200 |       </div>
201 |     </div>
202 |   </div>
203 |   <div class="fixed-grid has-2-cols">
204 |     <div class="grid">
205 |       <div class="cell">
206 |         <button
207 |           class="button is-link {isStreaming ? 'is-hidden' : ''}"
208 |           disabled={sessionStarted}
209 |           on:click={onStartSessionButtonClick}
210 |         >
211 |           <span class="icon">
212 |             <i class="fas fa-play"></i>
213 |           </span>
214 |           <span>{!sessionStarted ? L.start_a_new_session() : L.session_started()}</span>
215 |         </button>
216 |       </div>
217 | 
218 |       <div class="cell">
219 |         <button
220 |           class="button is-danger {!sessionStarted || isStreaming ? 'is-hidden' : ''}"
221 |           on:click={onDisconnectClick}
222 |         >
223 |           <span class="icon">
224 |             <i class="fas fa-unlink"></i>
225 |           </span>
226 |           <span>{L.cancel()}</span>
227 |         </button>
228 |       </div>
229 | 
230 |       <div class="cell">
231 |         <button
232 |           class="button is-link {!sessionStarted || isStreaming
233 |             ? 'is-hidden'
234 |             : ''} {copyButtonIsLoading ? 'is-loading' : ''}"
235 |           bind:this={copyButton}
236 |         >
237 |           <span class="icon">
238 |             <i class="fas fa-copy"></i>
239 |           </span>
240 |           <span>{L.copy_my_connection_string()}</span>
241 |         </button>
242 |       </div>
243 |     </div>
244 | 
245 |     <div class="field has-addons {!sessionStarted || isStreaming ? 'is-hidden' : ''}">
246 |       <div class="control has-icons-left has-icons-right">
247 |         <input
248 |           bind:value={$connectionString}
249 |           placeholder="participant connection string"
250 |           class="input {connectionStringIsValid === null
251 |             ? ''
252 |             : connectionStringIsValid
253 |               ? 'is-success'
254 |               : 'is-danger'}"
255 |           type="text"
256 |         />
257 |         <span class="icon is-small is-left">
258 |           <i class="fas fa-user"></i>
259 |         </span>
260 |         <span class="icon is-small is-right">
261 |           <i
262 |             class="fas fa-question {connectionStringIsValid === null
263 |               ? 'fa-question'
264 |               : connectionStringIsValid
265 |                 ? 'fa-check'
266 |                 : 'fa-times'}"
267 |           ></i>
268 |         </span>
269 |       </div>
270 |       <div class="control">
271 |         <button
272 |           class="button {connectionStringIsValid === null
273 |             ? 'is-link'
274 |             : connectionStringIsValid
275 |               ? 'is-success'
276 |               : 'is-danger'}"
277 |           bind:this={connectButton}
278 |           disabled={connectionStringIsValid ? false : true}
279 |         >
280 |           <span class="icon">
281 |             <i class="fas fa-link"></i>
282 |           </span>
283 |           <span>{L.connect()} {connectionStringIsValid ? connectToUserName : ''} </span>
284 |         </button>
285 |       </div>
286 |     </div>
287 |   </div>
288 | </div>
289 | 


--------------------------------------------------------------------------------
/src/renderer/src/Join.svelte:
--------------------------------------------------------------------------------
  1 | <script lang="ts">
  2 |   import { onMount } from 'svelte'
  3 |   import Swal from 'sweetalert2'
  4 |   import { L } from './translations'
  5 |   import {
  6 |     makeVideoDraggable,
  7 |     mayBeConnectionString,
  8 |     getDataFromBananasUrl,
  9 |     ConnectionType,
 10 |     getUUIDv4
 11 |   } from './Utils'
 12 |   import { useNavigationEnabled, useIsWatching, useParticipantUrl } from './stores'
 13 |   import WebRTC from './WebRTC.svelte'
 14 |   import AudioVisualizer from './AudioVisualizer.svelte'
 15 | 
 16 |   const navigationEnabled = useNavigationEnabled()
 17 |   const isWatching = useIsWatching()
 18 | 
 19 |   let connectionState = 'disconnected'
 20 |   let webRTCComponent: WebRTC
 21 |   let connectButton: HTMLButtonElement
 22 |   let copyButton: HTMLButtonElement
 23 |   let remoteScreen: HTMLVideoElement
 24 |   let UUID = getUUIDv4()
 25 |   let zoomFactor = 1
 26 |   let microphoneActive = false
 27 |   let isStreaming = false
 28 |   let isConnected = false
 29 |   let connectionStringIsValid: boolean | null = null
 30 |   let connectToUserName = ''
 31 |   let copyButtonIsLoading = false
 32 |   let connectionString = useParticipantUrl()
 33 |   let visualizerIsActive: boolean = true
 34 | 
 35 |   const onConnectionStringChange = async (): Promise<void> => {
 36 |     if ($connectionString === '') {
 37 |       connectionStringIsValid = null
 38 |       return
 39 |     }
 40 |     connectionStringIsValid = mayBeConnectionString(ConnectionType.HOST, $connectionString)
 41 |     if (connectionStringIsValid) {
 42 |       const bananasData = await getDataFromBananasUrl($connectionString)
 43 |       connectToUserName = bananasData.data.username
 44 |     }
 45 |   }
 46 | 
 47 |   const onConnectionStateChange = (): void => {
 48 |     switch (connectionState) {
 49 |       case 'connected':
 50 |         Swal.fire({
 51 |           position: 'top-end',
 52 |           icon: 'success',
 53 |           title: 'Connection established',
 54 |           showConfirmButton: false,
 55 |           timer: 1500
 56 |         })
 57 |         break
 58 |       case 'failed':
 59 |         Swal.fire({
 60 |           position: 'top-end',
 61 |           icon: 'error',
 62 |           title: 'Connection failed',
 63 |           showConfirmButton: false,
 64 |           timer: 1500
 65 |         })
 66 |         break
 67 |       case 'closed':
 68 |         Swal.fire({
 69 |           position: 'top-end',
 70 |           icon: 'info',
 71 |           title: 'Connection closed',
 72 |           showConfirmButton: false,
 73 |           timer: 1500
 74 |         })
 75 |         break
 76 |       default:
 77 |         break
 78 |     }
 79 |   }
 80 | 
 81 |   $: $connectionString, onConnectionStringChange()
 82 |   $: connectionState, onConnectionStateChange()
 83 | 
 84 |   onMount(async () => {
 85 |     const settings = await window.BananasApi.getSettings()
 86 |     microphoneActive = settings.isMicrophoneEnabledOnConnect
 87 |     makeVideoDraggable(remoteScreen)
 88 |     connectButton.addEventListener('click', async () => {
 89 |       await webRTCComponent.Setup(remoteScreen)
 90 |       const data = await getDataFromBananasUrl($connectionString)
 91 |       await webRTCComponent.Connect(data.rtcSessionDescription)
 92 |       isConnected = true
 93 |       $isWatching = true
 94 |       $navigationEnabled = false
 95 |     })
 96 |     copyButton.addEventListener('click', async () => {
 97 |       copyButtonIsLoading = true
 98 |       const remoteData = await getDataFromBananasUrl($connectionString)
 99 |       const data = await webRTCComponent.CreateParticipantUrl(remoteData.rtcSessionDescription, {
100 |         username: settings.username
101 |       })
102 |       navigator.clipboard.writeText(data)
103 |       setTimeout(() => {
104 |         copyButtonIsLoading = false
105 |       }, 400)
106 |     })
107 |     remoteScreen.addEventListener('dblclick', () => {
108 |       webRTCComponent.PingRemoteCursor('cursor-' + UUID)
109 |     })
110 |     remoteScreen.addEventListener('mousemove', (e) => {
111 |       const { offsetX, offsetY } = e
112 |       // TODO: Batch cursor updates
113 |       webRTCComponent.UpdateRemoteCursor({
114 |         x: offsetX / remoteScreen.clientWidth,
115 |         y: offsetY / remoteScreen.clientHeight,
116 |         name: settings.username,
117 |         id: 'cursor-' + UUID,
118 |         color: settings.color
119 |       })
120 |     })
121 |     remoteScreen.addEventListener('play', () => {
122 |       if (!webRTCComponent.IsConnected()) return
123 |       isStreaming = true
124 |     })
125 |   })
126 |   const reset = (): void => {
127 |     $connectionString = ''
128 |     connectionStringIsValid = null
129 |     isStreaming = false
130 |     microphoneActive = false
131 |     isConnected = false
132 |     $navigationEnabled = true
133 |     $isWatching = false
134 |   }
135 |   const onDisconnectClick = async (): Promise<void> => {
136 |     await webRTCComponent.Disconnect()
137 |     reset()
138 |   }
139 |   const onFullscreenClick = (): void => {
140 |     remoteScreen.requestFullscreen()
141 |   }
142 |   const onZoomInClick = (): void => {
143 |     zoomFactor += 0.1
144 |     remoteScreen.style.scale = zoomFactor.toString()
145 |   }
146 |   const onZoomOutClick = (): void => {
147 |     if (zoomFactor <= 1) return
148 |     zoomFactor -= 0.1
149 |     remoteScreen.style.scale = zoomFactor.toString()
150 |   }
151 |   const onMicrophoneToggle = async (): Promise<void> => {
152 |     microphoneActive = !microphoneActive
153 |     webRTCComponent.ToggleMicrophone()
154 |   }
155 | </script>
156 | 
157 | <WebRTC bind:connectionState bind:this={webRTCComponent} />
158 | 
159 | <div class="container p-5">
160 |   <h1 class="title">{!isStreaming ? L.join_a_session() : L.joined_a_session()}</h1>
161 |   <div class={!isStreaming ? 'is-hidden' : ''}>
162 |     <div class="fixed-grid">
163 |       <div class="grid">
164 |         <div class="cell">
165 |           <button
166 |             aria-label="{microphoneActive ? L.microphone_active() : L.microphone_inactive()}}"
167 |             title={microphoneActive ? L.microphone_active() : L.microphone_inactive()}
168 |             class="button {microphoneActive ? 'is-success' : 'is-danger'}"
169 |             on:click={onMicrophoneToggle}
170 |           >
171 |             <span class="icon">
172 |               {#if microphoneActive}
173 |                 <AudioVisualizer
174 |                   className="icon {!visualizerIsActive ? 'is-hidden' : ''}"
175 |                   bind:visualizerIsActive
176 |                   stream={webRTCComponent.GetAudioStream()}
177 |                 />
178 |                 <i class="fas fa-microphone {visualizerIsActive ? 'is-hidden' : ''}"></i>
179 |               {:else}
180 |                 <i class="fas fa-microphone-slash"></i>
181 |               {/if}
182 |             </span>
183 |           </button>
184 |         </div>
185 |         <div class="cell has-text-right">
186 |           <button class="button is-danger" aria-label={L.disconnect()} on:click={onDisconnectClick}>
187 |             <span class="icon">
188 |               <i class="fas fa-unlink"></i>
189 |             </span>
190 |             <span>{L.disconnect()}</span>
191 |           </button>
192 |         </div>
193 |       </div>
194 |     </div>
195 |   </div>
196 |   <div class="fixed-grid has-2-cols">
197 |     <div class="grid">
198 |       <div class="cell">
199 |         <div class="field has-addons {isStreaming || isConnected ? 'is-hidden' : ''}">
200 |           <div class="control has-icons-left has-icons-right">
201 |             <input
202 |               bind:value={$connectionString}
203 |               placeholder={L.host_connection_string()}
204 |               class="input {connectionStringIsValid === null
205 |                 ? ''
206 |                 : connectionStringIsValid
207 |                   ? 'is-success'
208 |                   : 'is-danger'}"
209 |               type="text"
210 |             />
211 |             <span class="icon is-small is-left">
212 |               <i class="fas fa-user"></i>
213 |             </span>
214 |             <span class="icon is-small is-right">
215 |               <i
216 |                 class="fas fa-question {connectionStringIsValid === null
217 |                   ? 'fa-question'
218 |                   : connectionStringIsValid
219 |                     ? 'fa-check'
220 |                     : 'fa-times'}"
221 |               ></i>
222 |             </span>
223 |           </div>
224 |           <div class="control">
225 |             <button
226 |               class="button {connectionStringIsValid === null
227 |                 ? 'is-link'
228 |                 : connectionStringIsValid
229 |                   ? 'is-success'
230 |                   : 'is-danger'}"
231 |               bind:this={connectButton}
232 |               disabled={connectionStringIsValid ? false : true}
233 |             >
234 |               <span class="icon">
235 |                 <i class="fas fa-link"></i>
236 |               </span>
237 |               <span>{L.connect()} {connectionStringIsValid ? connectToUserName : ''} </span>
238 |             </button>
239 |           </div>
240 |         </div>
241 |         <div class="control">
242 |           <button
243 |             class="button is-link {!isConnected || isStreaming
244 |               ? 'is-hidden'
245 |               : ''} {copyButtonIsLoading ? 'is-loading' : ''}"
246 |             bind:this={copyButton}
247 |           >
248 |             <span class="icon">
249 |               <i class="fas fa-copy"></i>
250 |             </span>
251 |             <span>{L.copy_my_connection_string()}</span>
252 |           </button>
253 |         </div>
254 |       </div>
255 |       <div class="cell">
256 |         <button
257 |           class="button is-danger {isStreaming || !isConnected ? 'is-hidden' : ''}"
258 |           on:click={onDisconnectClick}
259 |         >
260 |           <span class="icon">
261 |             <i class="fas fa-unlink"></i>
262 |           </span>
263 |           <span>{L.cancel()}</span>
264 |         </button>
265 |       </div>
266 |     </div>
267 |   </div>
268 | </div>
269 | 
270 | <div class={!isStreaming ? 'is-hidden' : ''}>
271 |   <div class="field">
272 |     <label class="label" for="remote_screen">{L.remote_screen()}</label>
273 |     <div class="control">
274 |       <div class="video-overflow">
275 |         <video bind:this={remoteScreen} id="remote_screen" class="video" autoplay playsinline muted
276 |         ></video>
277 |       </div>
278 |     </div>
279 |   </div>
280 |   <div class="field">
281 |     <div class="control">
282 |       <button class="button is-info" on:click={onZoomInClick}>
283 |         <span class="icon">
284 |           <i class="fas fa-search-plus"></i>
285 |         </span>
286 |         <span>{L.zoom_in()}</span>
287 |       </button>
288 |       <button class="button is-info" on:click={onZoomOutClick}>
289 |         <span class="icon">
290 |           <i class="fas fa-search-minus"></i>
291 |         </span>
292 |         <span>{L.zoom_out()}</span>
293 |       </button>
294 |       <button class="button is-info" on:click={onFullscreenClick}>
295 |         <span class="icon">
296 |           <i class="fas fa-expand"></i>
297 |         </span>
298 |         <span>{L.fullscreen()}</span>
299 |       </button>
300 |     </div>
301 |   </div>
302 | </div>
303 | 
304 | <style>
305 |   .video {
306 |     width: 100%;
307 |     height: auto;
308 |     transition: transform 0.5s linear;
309 |   }
310 |   .video-overflow {
311 |     width: 100%;
312 |     height: auto;
313 |     overflow: hidden;
314 |   }
315 | </style>
316 | 


--------------------------------------------------------------------------------
/src/renderer/src/Navigation.svelte:
--------------------------------------------------------------------------------
 1 | <script lang="ts">
 2 |   import { useActiveView, useNavigationEnabled, useIsWatching, useIsHosting } from './stores'
 3 |   import { L } from './translations'
 4 |   const activeView = useActiveView()
 5 |   const isHosting = useIsHosting()
 6 |   const isWatching = useIsWatching()
 7 |   const navigationEnabled = useNavigationEnabled()
 8 | 
 9 |   const handleTopButtonsClick = (evt: MouseEvent): void => {
10 |     evt.preventDefault()
11 |     const target = evt.target as HTMLButtonElement
12 |     const root = target.closest('button')
13 | 
14 |     $activeView = root.dataset.action
15 |   }
16 | </script>
17 | 
18 | <div class="container">
19 |   <nav class="navbar" aria-label="main navigation">
20 |     <div class="navbar-brand p-2">
21 |       <div class="navbar-start">
22 |         <div class="navbar-item">
23 |           <div class="buttons">
24 |             <button
25 |               class="button {$activeView === 'join' ? 'is-active is-primary' : 'is-secondary'}"
26 |               data-action="join"
27 |               on:click={handleTopButtonsClick}
28 |               disabled={!$navigationEnabled}
29 |             >
30 |               <span class="icon">
31 |                 <i class="fa-solid fa-right-to-bracket"></i>
32 |               </span>
33 |               <strong>{!$isWatching ? L.join_a_session() : L.joined_a_session()}</strong>
34 |             </button>
35 |             <button
36 |               class="button is-secondary {$activeView === 'host'
37 |                 ? 'is-active is-primary'
38 |                 : 'is-secondary'}"
39 |               data-action="host"
40 |               on:click={handleTopButtonsClick}
41 |               disabled={!$navigationEnabled}
42 |             >
43 |               <span class="icon">
44 |                 <i class="fa-solid fa-earth-africa"></i>
45 |               </span>
46 |               <strong>{!$isHosting ? L.host_a_session() : L.hosting_a_session()}</strong>
47 |             </button>
48 |             <button
49 |               class="button is-secondary {$activeView === 'settings'
50 |                 ? 'is-active is-primary'
51 |                 : 'is-secondary'}"
52 |               data-action="settings"
53 |               on:click={handleTopButtonsClick}
54 |               disabled={!$navigationEnabled}
55 |             >
56 |               <span class="icon">
57 |                 <i class="fa-solid fa-gear"></i>
58 |               </span>
59 |               <strong>{L.settings()}</strong>
60 |             </button>
61 |             <button
62 |               class="button is-secondary {$activeView === 'about'
63 |                 ? 'is-active is-primary'
64 |                 : 'is-secondary'}"
65 |               data-action="about"
66 |               on:click={handleTopButtonsClick}
67 |               disabled={!$navigationEnabled}
68 |             >
69 |               <span class="icon">
70 |                 <i class="fa-solid fa-question"></i>
71 |               </span>
72 |               <strong>{L.about()}</strong>
73 |             </button>
74 |           </div>
75 |         </div>
76 |       </div>
77 |     </div>
78 |   </nav>
79 | </div>
80 | 


--------------------------------------------------------------------------------
/src/renderer/src/Settings.svelte:
--------------------------------------------------------------------------------
  1 | <script lang="ts">
  2 |   import { onMount } from 'svelte'
  3 |   import ColorPicker from 'svelte-awesome-color-picker'
  4 |   import { L } from './translations'
  5 | 
  6 |   let colorPreviewIcon: HTMLElement
  7 |   let usernameValue: string = 'Banana Joe'
  8 |   let colorValue: string = '#ffffff'
  9 |   let language = 'en'
 10 |   const languageOptions = ['en', 'de', 'fr', 'zh']
 11 |   let iceServersValue: string = '{ "urls": "stun:stun.l.google.com:19302" }'
 12 |   let isUsernameValid = false
 13 |   let isColorValid = false
 14 |   let isIceServersValid = true
 15 |   let modalSuccessIsActive = false
 16 |   let modalFailureIsActive = false
 17 |   let isMicrophoneEnabledOnConnect = true
 18 | 
 19 |   $: colorValue, checkColor()
 20 |   $: usernameValue, checkUsername()
 21 |   $: iceServersValue, checkIceServers()
 22 |   $: isMicrophoneEnabledOnConnect
 23 | 
 24 |   const checkIceServers = (): void => {
 25 |     const serversObjects = iceServersValue.split('\n')
 26 |     isIceServersValid = serversObjects.every((serverObject) => {
 27 |       try {
 28 |         const srv = JSON.parse(serverObject)
 29 |         return srv.urls && srv.urls.length > 0
 30 |       } catch (e) {
 31 |         return false
 32 |       }
 33 |     })
 34 |   }
 35 |   const checkIsValidHexColor = (color: string): boolean => {
 36 |     return /^#[0-9A-F]{6}$/i.test(color)
 37 |   }
 38 |   function checkColor(): void {
 39 |     if (checkIsValidHexColor(colorValue)) {
 40 |       isColorValid = true
 41 |       colorPreviewIcon?.style.setProperty('--color', colorValue)
 42 |     } else {
 43 |       isColorValid = false
 44 |     }
 45 |   }
 46 |   function checkUsername(): void {
 47 |     if (usernameValue.length > 0 && usernameValue.length < 32) {
 48 |       isUsernameValid = true
 49 |     } else {
 50 |       isUsernameValid = false
 51 |     }
 52 |   }
 53 |   async function onSubmit(evt: Event): Promise<void> {
 54 |     evt.preventDefault()
 55 |     if (isUsernameValid && isColorValid && isIceServersValid) {
 56 |       await window.BananasApi.updateSettings({
 57 |         username: usernameValue,
 58 |         color: colorValue,
 59 |         language,
 60 |         isMicrophoneEnabledOnConnect,
 61 |         iceServers: iceServersValue.split('\n').map((srv) => JSON.parse(srv))
 62 |       })
 63 |       modalSuccessIsActive = true
 64 |       setTimeout(() => {
 65 |         modalSuccessIsActive = false
 66 |       }, 2000)
 67 |     } else {
 68 |       modalFailureIsActive = true
 69 |       setTimeout(() => {
 70 |         modalFailureIsActive = false
 71 |       }, 2000)
 72 |     }
 73 |   }
 74 |   onMount(async () => {
 75 |     const settings = await window.BananasApi.getSettings()
 76 |     usernameValue = settings.username
 77 |     colorValue = settings.color
 78 |     language = settings.language
 79 |     isMicrophoneEnabledOnConnect = settings.isMicrophoneEnabledOnConnect
 80 |     iceServersValue = settings.iceServers.map((srv) => JSON.stringify(srv)).join('\n')
 81 |   })
 82 | </script>
 83 | 
 84 | <div class="modal {modalSuccessIsActive ? 'is-active' : ''}">
 85 |   <div class="modal-background"></div>
 86 |   <div class="modal-content">
 87 |     <div class="box">
 88 |       <h1 class="title has-text-success">Success</h1>
 89 |       <p>Settings successfully saved.</p>
 90 |     </div>
 91 |   </div>
 92 | </div>
 93 | 
 94 | <div class="modal {modalFailureIsActive ? 'is-active' : ''}">
 95 |   <div class="modal-background"></div>
 96 |   <div class="modal-content">
 97 |     <div class="box">
 98 |       <h1 class="title has-text-danger">Failure</h1>
 99 |       <p>Settings could not be saved.</p>
100 |     </div>
101 |   </div>
102 | </div>
103 | 
104 | <div class="container p-5 content">
105 |   <h1 class="title">{L.settings()}</h1>
106 |   <h2>{L.basic()}</h2>
107 |   <form class="form" on:submit={onSubmit}>
108 |     <div class="field">
109 |       <label class="label" for="username">{L.username()}</label>
110 |       <div class="control has-icons-left has-icons-right">
111 |         <input
112 |           bind:value={usernameValue}
113 |           class="input {isUsernameValid ? 'is-success' : 'is-danger'}"
114 |           type="text"
115 |           id="username"
116 |           placeholder="Banana Joe"
117 |         />
118 |         <span class="icon is-small is-left">
119 |           <i class="fas fa-user"></i>
120 |         </span>
121 |       </div>
122 |     </div>
123 | 
124 |     <div class="field">
125 |       <label class="label" for="color">{L.color()}</label>
126 |       <div class="control has-icons-left has-icons-right">
127 |         <input
128 |           bind:value={colorValue}
129 |           class="input {isColorValid ? 'is-success' : 'is-danger'}"
130 |           type="text"
131 |           id="color"
132 |           placeholder="#fffff"
133 |         />
134 |         <span class="icon is-small is-left">
135 |           <i bind:this={colorPreviewIcon} class="fas fa-palette"></i>
136 |         </span>
137 |         <ColorPicker bind:hex={colorValue} isTextInput={false} isAlpha={false} />
138 |       </div>
139 |     </div>
140 |     <div class="field">
141 |       <label class="label" for="translation">{L.language()}</label>
142 |       <div class="control has-icons-left has-icons-right">
143 |         <div class="select">
144 |           <select bind:value={language}>
145 |             {#each languageOptions as lang}
146 |               <option>{lang}</option>
147 |             {/each}
148 |           </select>
149 |         </div>
150 |         <span class="icon is-small is-left">
151 |           <i class="fa fa-language"></i>
152 |         </span>
153 |       </div>
154 |       <p class="help">{L.language_description()}</p>
155 |     </div>
156 | 
157 |     <h2>Media</h2>
158 | 
159 |     <div class="field">
160 |       <div class="control">
161 |         <label class="checkbox" for="microphone_active_on_connect">
162 |           <input
163 |             bind:checked={isMicrophoneEnabledOnConnect}
164 |             class="checkbox"
165 |             type="checkbox"
166 |             id="microphone_active_on_connect"
167 |             placeholder="#fffff"
168 |           />
169 |           {L.is_microphone_active_on_connect()}
170 |         </label>
171 |       </div>
172 |     </div>
173 | 
174 |     <h2>{L.advanced()}</h2>
175 | 
176 |     <div class="field">
177 |       <label class="label" for="ice_servers">{L.stun_turn_server_objects()}</label>
178 |       <div class="control has-icons-left has-icons-right">
179 |         <textarea
180 |           bind:value={iceServersValue}
181 |           class="textarea {isIceServersValid ? 'is-success' : 'is-danger'}"
182 |           id="ice_servers"
183 |           placeholder="&lbrace; &quot;urls&quot;: &quot;stun:stun.l.google.com:19302&quot; &rbrace;"
184 |         ></textarea>
185 |       </div>
186 |     </div>
187 | 
188 |     <div class="field">
189 |       <div class="control">
190 |         <button class="button is-link">Save</button>
191 |       </div>
192 |     </div>
193 |   </form>
194 | </div>
195 | 
196 | <style>
197 |   span.icon i.fa-palette:before {
198 |     color: var(--color);
199 |     text-shadow: '-1px 0 black, 0 1px black, 1px 0 black, 0 -1px black';
200 |   }
201 | </style>
202 | 


--------------------------------------------------------------------------------
/src/renderer/src/UseSharedStore.ts:
--------------------------------------------------------------------------------
 1 | import type { Readable, Writable } from 'svelte/store'
 2 | import { getContext, hasContext, setContext } from 'svelte'
 3 | import { readable, writable } from 'svelte/store'
 4 | 
 5 | export const useSharedStore = <T, A>(name: string, fn: (value?: A) => T, defaultValue?: A): T => {
 6 |   if (hasContext(name)) {
 7 |     return getContext<T>(name)
 8 |   }
 9 |   const _value = fn(defaultValue)
10 |   setContext(name, _value)
11 |   return _value
12 | }
13 | 
14 | export const useWritable = <T>(name: string, value: T): Writable<T> =>
15 |   useSharedStore(name, writable, value)
16 | 
17 | export const useReadable = <T>(name: string, value: T): Readable<T> =>
18 |   useSharedStore(name, readable, value)
19 | 


--------------------------------------------------------------------------------
/src/renderer/src/Utils.ts:
--------------------------------------------------------------------------------
  1 | export const enum ConnectionType {
  2 |   HOST = 'host',
  3 |   PARTICIPANT = 'participant'
  4 | }
  5 | 
  6 | export type RTCSessionDescriptionOptions = RTCSessionDescriptionInit
  7 | 
  8 | export const externalLinkClickHandler = (root: HTMLButtonElement, url: string): void => {
  9 |   root.classList.add('is-loading')
 10 |   setTimeout(() => {
 11 |     root.classList.remove('is-loading')
 12 |   }, 3000)
 13 |   window.open(url)
 14 | }
 15 | 
 16 | export const getUUIDv4 = (): string => {
 17 |   return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
 18 |     const r = (Math.random() * 16) | 0
 19 |     const v = c === 'x' ? r : (r & 0x3) | 0x8
 20 |     return v.toString(16)
 21 |   })
 22 | }
 23 | 
 24 | /* eslint-disable @typescript-eslint/no-explicit-any */
 25 | export const compressJson = async (data: any): Promise<string> => {
 26 |   const stream = new Blob([JSON.stringify(data)], {
 27 |     type: 'application/json'
 28 |   }).stream()
 29 |   const compressedStream = stream.pipeThrough(new CompressionStream('gzip'))
 30 |   const compressedResponse = new Response(compressedStream)
 31 |   const blob = await compressedResponse.blob()
 32 |   const buffer = await blob.arrayBuffer()
 33 |   return btoa(String.fromCharCode(...new Uint8Array(buffer)))
 34 | }
 35 | 
 36 | /* eslint-disable @typescript-eslint/no-explicit-any */
 37 | export const decompressJson = async (data: string): Promise<any> => {
 38 |   const buffer = new Uint8Array(
 39 |     atob(data)
 40 |       .split('')
 41 |       .map((c) => c.charCodeAt(0))
 42 |   )
 43 |   const stream = new Blob([buffer], {
 44 |     type: 'application/json'
 45 |   }).stream()
 46 |   const decompressedStream = stream.pipeThrough(new DecompressionStream('gzip'))
 47 |   const res = new Response(decompressedStream)
 48 |   const blob = await res.blob()
 49 |   return JSON.parse(await blob.text())
 50 | }
 51 | 
 52 | export const mayBeConnectionString = (ct: ConnectionType, str: string): boolean => {
 53 |   try {
 54 |     const url = new URL(str)
 55 |     if (url.protocol !== 'bananas:') return false
 56 |     if (url.pathname.slice(2) !== ct) return false
 57 |     const token = url.searchParams.get('token')
 58 |     const username = url.searchParams.get('username')
 59 |     if (!token || !username) return false
 60 |     decompressJson(token)
 61 |     return true
 62 |   } catch (err) {
 63 |     return false
 64 |   }
 65 | }
 66 | 
 67 | export const getConnectionString = async (
 68 |   ct: ConnectionType,
 69 |   offer: RTCSessionDescriptionInit,
 70 |   data: {
 71 |     username: string
 72 |   }
 73 | ): Promise<string> => {
 74 |   const { username } = data
 75 |   const token = encodeURIComponent(await compressJson(offer))
 76 |   return `bananas://${ct}?username=${encodeURIComponent(username)}&token=${token}`
 77 | }
 78 | 
 79 | export const getDataFromBananasUrl = async (
 80 |   url: string
 81 | ): Promise<{
 82 |   type: ConnectionType
 83 |   data: { username: string }
 84 |   rtcSessionDescription: RTCSessionDescriptionInit
 85 | }> => {
 86 |   const u = new URL(url)
 87 |   const token = u.searchParams.get('token')
 88 |   const username = u.searchParams.get('username')
 89 |   return {
 90 |     type: u.pathname.slice(2) as ConnectionType,
 91 |     data: {
 92 |       username: username
 93 |     },
 94 |     rtcSessionDescription: await decompressJson(decodeURIComponent(token))
 95 |   }
 96 | }
 97 | 
 98 | export const makeVideoDraggable = (video: HTMLVideoElement): void => {
 99 |   let startX: number
100 |   let startY: number
101 |   let initialX: number
102 |   let initialY: number
103 |   let isDragging = false
104 |   video.addEventListener('mousedown', (e) => {
105 |     isDragging = true
106 |     startX = e.clientX
107 |     startY = e.clientY
108 |     const transform = getComputedStyle(video).transform
109 | 
110 |     if (transform !== 'none') {
111 |       const values = transform.split('(')[1].split(')')[0].split(',')
112 |       initialX = parseFloat(values[4])
113 |       initialY = parseFloat(values[5])
114 |     } else {
115 |       initialX = 0
116 |       initialY = 0
117 |     }
118 |     video.style.cursor = 'grabbing'
119 |   })
120 |   document.addEventListener('mousemove', (e) => {
121 |     if (!isDragging) return
122 | 
123 |     const deltaX = e.clientX - startX
124 |     const deltaY = e.clientY - startY
125 | 
126 |     const moveX = initialX + deltaX
127 |     const moveY = initialY + deltaY
128 | 
129 |     video.style.transform = `translate(${moveX}px, ${moveY}px)`
130 |   })
131 | 
132 |   document.addEventListener('mouseup', () => {
133 |     if (!isDragging) return
134 |     isDragging = false
135 |     video.style.cursor = 'default'
136 |   })
137 | }
138 | 
139 | export const debounce = <T extends (...args: unknown[]) => void>(
140 |   func: T,
141 |   wait: number
142 | ): ((...args: Parameters<T>) => void) => {
143 |   let timeout: ReturnType<typeof setTimeout>
144 |   return (...args: Parameters<T>): void => {
145 |     clearTimeout(timeout)
146 |     timeout = setTimeout(() => {
147 |       func(...args)
148 |     }, wait)
149 |   }
150 | }
151 | 
152 | export const throttle = <T extends (...args: unknown[]) => void>(
153 |   func: T,
154 |   wait: number
155 | ): ((...args: Parameters<T>) => void) => {
156 |   let lastCalled = 0
157 |   return (...args: Parameters<T>): void => {
158 |     const now = Date.now()
159 |     if (now - lastCalled < wait) return
160 |     lastCalled = now
161 |     func(...args)
162 |   }
163 | }
164 | 


--------------------------------------------------------------------------------
/src/renderer/src/WebRTC.svelte:
--------------------------------------------------------------------------------
  1 | <script lang="ts">
  2 |   import type { RTCSessionDescriptionOptions } from './Utils'
  3 |   import type { BananasRemoteCursorData, SettingsData } from './BananasTypes'
  4 |   import { getConnectionString, ConnectionType } from './Utils'
  5 |   import { getRTCPeerConnectionConfig } from './Config'
  6 | 
  7 |   export let connectionState: string = 'disconnected'
  8 | 
  9 |   const errorHander = (e: ErrorEvent): void => {
 10 |     console.error(e)
 11 |   }
 12 | 
 13 |   let remoteVideo: HTMLVideoElement | null = null
 14 |   let pc: RTCPeerConnection | null = null
 15 |   let remoteCursorPositionsEnabled = false
 16 |   let remoteMouseCursorPositionsChannel: RTCDataChannel | null = null
 17 |   let remoteCursorPingChannel: RTCDataChannel | null = null
 18 |   let audioStream: MediaStream | null = null
 19 |   let stream: MediaStream | null = null
 20 |   let audioElement: HTMLAudioElement | null = null
 21 |   let userSettings: SettingsData | null = null
 22 | 
 23 |   const remoteMouseCursorPositionsChannelIsReady = (): boolean => {
 24 |     if (!remoteMouseCursorPositionsChannel) return false
 25 |     if (remoteMouseCursorPositionsChannel.readyState === 'open') return true
 26 |     return false
 27 |   }
 28 | 
 29 |   const remoteCursorPingChannelIsReady = (): boolean => {
 30 |     if (!remoteCursorPingChannel) return false
 31 |     if (remoteCursorPingChannel.readyState === 'open') return true
 32 |     return false
 33 |   }
 34 | 
 35 |   const setupDataChannel = (dc: RTCDataChannel): void => {
 36 |     if (dc.label === 'remoteMouseCursorPositions') {
 37 |       remoteMouseCursorPositionsChannel = dc
 38 |       dc.onmessage = function (e: MessageEvent): void {
 39 |         if (!remoteCursorPositionsEnabled) return
 40 |         if (remoteVideo) return
 41 |         const data = JSON.parse(e.data)
 42 |         window.BananasApi.updateRemoteCursor(data)
 43 |       }
 44 |     }
 45 |     if (dc.label === 'remoteCursorPing') {
 46 |       remoteCursorPingChannel = dc
 47 |       dc.onmessage = function (e: MessageEvent): void {
 48 |         if (!remoteCursorPositionsEnabled) return
 49 |         if (remoteVideo) return
 50 |         window.BananasApi.remoteCursorPing(e.data)
 51 |       }
 52 |     }
 53 |   }
 54 |   export function PingRemoteCursor(cursorId: string): void {
 55 |     if (!remoteCursorPingChannelIsReady()) {
 56 |       console.error('remoteCursorPingChannel not ready')
 57 |       return
 58 |     }
 59 |     remoteCursorPingChannel.send(cursorId)
 60 |   }
 61 |   export function UpdateRemoteCursor(cursorData: BananasRemoteCursorData): void {
 62 |     if (!remoteMouseCursorPositionsChannelIsReady()) {
 63 |       console.error('remoteMouseCursorPositionsChannel not ready')
 64 |       return
 65 |     }
 66 |     remoteMouseCursorPositionsChannel.send(JSON.stringify(cursorData))
 67 |   }
 68 |   export function HasAudioInput(): boolean {
 69 |     return audioStream !== null
 70 |   }
 71 |   export function GetAudioStream(): MediaStream | null {
 72 |     return audioStream
 73 |   }
 74 |   export function ToggleRemoteCursors(enabled: boolean): boolean {
 75 |     if (!remoteMouseCursorPositionsChannel) return false
 76 |     if (remoteMouseCursorPositionsChannel.readyState !== 'open') return false
 77 |     remoteCursorPositionsEnabled = enabled
 78 |     return enabled
 79 |   }
 80 |   export async function Setup(v: HTMLVideoElement = null): Promise<void> {
 81 |     userSettings = await window.BananasApi.getSettings()
 82 |     remoteVideo = v
 83 |     audioElement = document.createElement('audio')
 84 |     audioElement.controls = true
 85 |     audioElement.autoplay = true
 86 |     if (pc) {
 87 |       pc.close()
 88 |       pc = null
 89 |     }
 90 |     pc = new RTCPeerConnection(await getRTCPeerConnectionConfig())
 91 |     pc.ondatachannel = (e: RTCDataChannelEvent): void => {
 92 |       if (e.channel.label === 'remoteMouseCursorPositions') {
 93 |         setupDataChannel(e.channel)
 94 |       }
 95 |       if (e.channel.label === 'remoteCursorPing') {
 96 |         setupDataChannel(e.channel)
 97 |       }
 98 |     }
 99 |     pc.ontrack = (evt): void => {
100 |       if (remoteVideo) {
101 |         remoteVideo.srcObject = evt.streams[0]
102 |       }
103 |       if (audioStream) {
104 |         audioElement.srcObject = evt.streams[0]
105 |       }
106 |     }
107 |     pc.onicecandidate = function (e: RTCPeerConnectionIceEvent): void {
108 |       const cand = e.candidate
109 |       if (!cand) {
110 |         console.log('icecandidate gathering: complete')
111 |       } else {
112 |         console.log('new icecandidate')
113 |       }
114 |     }
115 |     pc.oniceconnectionstatechange = function (): void {
116 |       connectionState = pc.iceConnectionState
117 |     }
118 |     try {
119 |       audioStream = await navigator.mediaDevices.getUserMedia({
120 |         video: false,
121 |         audio: true
122 |       })
123 |     } catch (e) {
124 |       errorHander(e)
125 |     }
126 |     if (!remoteVideo) {
127 |       try {
128 |         stream = await navigator.mediaDevices.getDisplayMedia({
129 |           video: true,
130 |           audio: false
131 |         })
132 |         for (const track of stream.getTracks()) {
133 |           pc.addTrack(track, stream)
134 |         }
135 |         if (audioStream) {
136 |           for (const track of audioStream.getTracks()) {
137 |             track.enabled = userSettings.isMicrophoneEnabledOnConnect
138 |             pc.addTrack(track, stream)
139 |           }
140 |         }
141 |       } catch (e) {
142 |         errorHander(e)
143 |       }
144 |     } else {
145 |       if (audioStream) {
146 |         for (const track of audioStream.getTracks()) {
147 |           track.enabled = userSettings.isMicrophoneEnabledOnConnect
148 |           pc.addTrack(track, audioStream)
149 |         }
150 |       }
151 |     }
152 |   }
153 |   export async function CreateParticipantUrl(
154 |     c: RTCSessionDescriptionOptions,
155 |     data: { username: string }
156 |   ): Promise<string> {
157 |     try {
158 |       const desc = new RTCSessionDescription(c)
159 |       await pc.setRemoteDescription(desc)
160 |       if (desc.type === 'offer') {
161 |         const answer = await pc.createAnswer()
162 |         await pc.setLocalDescription(answer)
163 |       }
164 |     } catch (e) {
165 |       errorHander(e)
166 |     }
167 |     return await getConnectionString(ConnectionType.PARTICIPANT, pc.localDescription, data)
168 |   }
169 |   export async function CreateHostUrl(data: { username: string }): Promise<string> {
170 |     remoteMouseCursorPositionsChannel = pc.createDataChannel('remoteMouseCursorPositions')
171 |     remoteCursorPingChannel = pc.createDataChannel('remoteCursorPing')
172 |     setupDataChannel(remoteMouseCursorPositionsChannel)
173 |     setupDataChannel(remoteCursorPingChannel)
174 |     const desc = await pc.createOffer()
175 |     await pc.setLocalDescription(desc)
176 |     return await getConnectionString(ConnectionType.HOST, pc.localDescription, data)
177 |   }
178 |   export function ToggleDisplayStream(): void {
179 |     if (stream) {
180 |       for (const track of stream.getVideoTracks()) {
181 |         track.enabled = !track.enabled
182 |       }
183 |     }
184 |   }
185 |   export function ToggleMicrophone(): void {
186 |     if (audioStream) {
187 |       for (const track of audioStream.getAudioTracks()) {
188 |         track.enabled = !track.enabled
189 |       }
190 |     }
191 |   }
192 |   export function IsMicrophoneActive(): boolean {
193 |     if (audioStream) {
194 |       for (const track of audioStream.getAudioTracks()) {
195 |         return track.enabled
196 |       }
197 |     }
198 |     return false
199 |   }
200 |   export async function Connect(c: RTCSessionDescriptionOptions): Promise<void> {
201 |     try {
202 |       const desc = new RTCSessionDescription(c)
203 |       await pc.setRemoteDescription(desc)
204 |       if (desc.type === 'offer') {
205 |         const answer = await pc.createAnswer()
206 |         await pc.setLocalDescription(answer)
207 |       }
208 |     } catch (e) {
209 |       errorHander(e)
210 |     }
211 |   }
212 |   export function IsConnected(): boolean {
213 |     return pc ? pc.connectionState === 'connected' : false
214 |   }
215 |   export async function Disconnect(): Promise<void> {
216 |     try {
217 |       pc.close()
218 |       pc = null
219 |       if (stream) {
220 |         for (const track of stream.getTracks()) {
221 |           track.stop()
222 |         }
223 |         stream = null
224 |       }
225 |       if (audioStream) {
226 |         for (const track of audioStream.getTracks()) {
227 |           track.stop()
228 |         }
229 |         audioStream = null
230 |       }
231 |     } catch (e) {
232 |       errorHander(e)
233 |     }
234 |   }
235 | </script>
236 | 


--------------------------------------------------------------------------------
/src/renderer/src/cursors.css:
--------------------------------------------------------------------------------
 1 | html,
 2 | body {
 3 |   overflow: hidden;
 4 | }
 5 | .cursor {
 6 |   display: block;
 7 |   position: absolute;
 8 |   width: 24px;
 9 |   height: 24px;
10 | }
11 | 
12 | .cursor .name {
13 |   display: block;
14 |   text-shadow: '-1px 0 black, 0 1px black, 1px 0 black, 0 -1px black';
15 |   color: var(--name-color);
16 | }
17 | 
18 | .cursor svg {
19 |   max-width: 24px;
20 |   fill: var(--cursor-color);
21 | }
22 | 
23 | .is-hidden {
24 |   display: none !important;
25 | }
26 | 
27 | .cursor .ping {
28 |   width: 48px;
29 |   height: 48px;
30 |   border: 5px solid var(--ping-color);
31 |   border-radius: 50%;
32 |   display: inline-block;
33 |   box-sizing: border-box;
34 |   position: absolute;
35 |   top: -12px;
36 |   left: -12px;
37 |   animation: pulse 1s linear infinite;
38 | }
39 | 
40 | .cursor .ping:after {
41 |   content: '';
42 |   position: absolute;
43 |   width: 48px;
44 |   height: 48px;
45 |   border: 5px solid var(--ping-color);
46 |   border-radius: 50%;
47 |   display: inline-block;
48 |   box-sizing: border-box;
49 |   left: 50%;
50 |   top: 50%;
51 |   transform: translate(-50%, -50%);
52 |   animation: scaleUp 1s linear infinite;
53 | }
54 | 
55 | @keyframes scaleUp {
56 |   0% {
57 |     transform: translate(-50%, -50%) scale(0);
58 |   }
59 |   60%,
60 |   100% {
61 |     transform: translate(-50%, -50%) scale(1);
62 |   }
63 | }
64 | @keyframes pulse {
65 |   0%,
66 |   60%,
67 |   100% {
68 |     transform: scale(1);
69 |   }
70 |   80% {
71 |     transform: scale(1.2);
72 |   }
73 | }
74 | 


--------------------------------------------------------------------------------
/src/renderer/src/env.d.ts:
--------------------------------------------------------------------------------
 1 | /// <reference types="svelte" />
 2 | /// <reference types="vite/client" />
 3 | import { ElectronAPI } from '@electron-toolkit/preload'
 4 | 
 5 | type IceServer = {
 6 |   urls: string
 7 |   username?: string
 8 |   credential?: string
 9 | }
10 | 
11 | declare global {
12 |   interface Window {
13 |     electron: ElectronAPI
14 |     BananasApi: {
15 |       toggleRemoteCursors: (state: boolean) => Promise<void>
16 |       remoteCursorPing: (cursorId: string) => Promise<void>
17 |       updateRemoteCursor: (state: {
18 |         id: string
19 |         name: string
20 |         color: string
21 |         x: number
22 |         y: number
23 |       }) => Promise<void>
24 |       updateSettings: (settings: {
25 |         username: string
26 |         language: string
27 |         color: string
28 |         isMicrophoneEnabledOnConnect: boolean
29 |         iceServers: IceServer[]
30 |       }) => Promise<void>
31 |       getSettings: () => Promise<{
32 |         username: string
33 |         color: string
34 |         language: string
35 |         isMicrophoneEnabledOnConnect: boolean
36 |         iceServers: IceServer[]
37 |       }>
38 |       getAppVersion: () => Promise<string>
39 |     }
40 |   }
41 | }
42 | 


--------------------------------------------------------------------------------
/src/renderer/src/main.ts:
--------------------------------------------------------------------------------
 1 | import '@fortawesome/fontawesome-free/css/all.min.css'
 2 | import 'bulma/css/bulma.min.css'
 3 | import '@sweetalert2/theme-bulma/bulma.min.css'
 4 | import App from './App.svelte'
 5 | import './overrides.css'
 6 | 
 7 | const app = new App({
 8 |   target: document.getElementById('app')
 9 | })
10 | 
11 | export default app
12 | 


--------------------------------------------------------------------------------
/src/renderer/src/overrides.css:
--------------------------------------------------------------------------------
 1 | html {
 2 |   overflow-y: auto;
 3 | }
 4 | 
 5 | span.color-picker div.wrapper.is-open {
 6 |   background-color: var(--bulma-border);
 7 | }
 8 | 
 9 | div.swal2-container div.swal2-popup {
10 |   background-color: var(--bulma-background);
11 | }
12 | 


--------------------------------------------------------------------------------
/src/renderer/src/stores.ts:
--------------------------------------------------------------------------------
 1 | import type { Writable } from 'svelte/store'
 2 | import { useWritable } from './UseSharedStore'
 3 | 
 4 | export const useActiveView = (): Writable<string> => useWritable('activeView', 'join')
 5 | 
 6 | export const useNavigationEnabled = (): Writable<boolean> => useWritable('navigationEnabled', true)
 7 | 
 8 | export const useIsHosting = (): Writable<boolean> => useWritable('isHosting', false)
 9 | 
10 | export const useIsWatching = (): Writable<boolean> => useWritable('useIsWatching', false)
11 | 
12 | export const useHostUrl = (): Writable<string> => useWritable('useHostUrl', '')
13 | 
14 | export const useParticipantUrl = (): Writable<string> => useWritable('useParticipantUrl', '')
15 | 


--------------------------------------------------------------------------------
/src/renderer/src/translations.ts:
--------------------------------------------------------------------------------
 1 | import { loadLocale } from '../../i18n/i18n-util.sync'
 2 | import { i18nObject } from '../../i18n/i18n-util'
 3 | import type { Locales } from '../../i18n/i18n-types'
 4 | 
 5 | const settings = await window.BananasApi.getSettings()
 6 | 
 7 | const locale = (settings?.language as Locales) || 'en'
 8 | 
 9 | loadLocale(locale)
10 | export const L = i18nObject(locale)
11 | 


--------------------------------------------------------------------------------
/svelte.config.mjs:
--------------------------------------------------------------------------------
 1 | import adapter from '@sveltejs/adapter-node'
 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
 3 | 
 4 | export default {
 5 |   kit: {
 6 |     adapter: adapter({})
 7 |   },
 8 |   extensions: ['.svelte'],
 9 |   preprocess: vitePreprocess()
10 | }
11 | 


--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "extends": "@electron-toolkit/tsconfig/tsconfig.json",
 3 |   "include": [
 4 |     "src/renderer/src/env.d.ts",
 5 |     "src/renderer/src/**/*",
 6 |     "src/renderer/src/**/*.svelte",
 7 |     "src/preload/*.d.ts"
 8 |   ],
 9 |   "compilerOptions": {
10 |     "verbatimModuleSyntax": true,
11 |     "useDefineForClassFields": true,
12 |     "strict": false,
13 |     "allowJs": true,
14 |     "checkJs": true,
15 |     "lib": ["ESNext", "DOM", "DOM.Iterable"]
16 |   },
17 |   "references": [{ "path": "./tsconfig.node.json" }]
18 | }
19 | 


--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 |   "extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
3 |   "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"],
4 |   "compilerOptions": {
5 |     "composite": true,
6 |     "types": ["electron-vite/node"]
7 |   }
8 | }
9 | 


--------------------------------------------------------------------------------