├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ └── feature-request.yml ├── stale.yml └── workflows │ ├── build-verify.yml │ ├── release-apk.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── Cargo.lock ├── Cargo.toml ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── angular.json ├── fastlane ├── .gitignore ├── Appfile ├── Fastfile ├── Pluginfile ├── README.md └── util │ └── tauri.rb ├── karma.conf.js ├── package-lock.json ├── package.json ├── resources ├── icon │ ├── android │ │ ├── debug │ │ │ └── drawable │ │ │ │ └── ic_launcher_background.xml │ │ └── main │ │ │ ├── drawable │ │ │ └── ic_launcher_background.xml │ │ │ └── mipmap │ │ │ └── ic_launcher.xml │ ├── desktop.svg │ ├── mobile.png │ └── mobile.svg ├── play_store_featured.svg └── splash.svg ├── scripts ├── icon-generator.js ├── karma-tauri-launcher.js ├── ng-wrapper.js └── tauri-wrapper.js ├── src-tauri ├── .gitignore ├── Cargo.toml ├── README.md ├── assets │ └── app.desktop ├── build.rs ├── capabilities │ └── app.json ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ └── ios │ │ ├── AppIcon-20x20@1x.png │ │ ├── AppIcon-20x20@2x-1.png │ │ ├── AppIcon-20x20@2x.png │ │ ├── AppIcon-20x20@3x.png │ │ ├── AppIcon-29x29@1x.png │ │ ├── AppIcon-29x29@2x-1.png │ │ ├── AppIcon-29x29@2x.png │ │ ├── AppIcon-29x29@3x.png │ │ ├── AppIcon-40x40@1x.png │ │ ├── AppIcon-40x40@2x-1.png │ │ ├── AppIcon-40x40@2x.png │ │ ├── AppIcon-40x40@3x.png │ │ ├── AppIcon-512@2x.png │ │ ├── AppIcon-60x60@2x.png │ │ ├── AppIcon-60x60@3x.png │ │ ├── AppIcon-76x76@1x.png │ │ ├── AppIcon-76x76@2x.png │ │ └── AppIcon-83.5x83.5@2x.png ├── permissions │ ├── dev-mode │ │ └── default.toml │ ├── device-manager │ │ └── default.toml │ ├── local-file │ │ └── default.toml │ ├── remote-command │ │ └── default.toml │ ├── remote-file │ │ └── default.toml │ └── remote-shell │ │ └── default.toml ├── src │ ├── app_dirs │ │ └── mod.rs │ ├── byte_string.rs │ ├── conn_pool │ │ ├── cmd.rs │ │ ├── connection.rs │ │ ├── mod.rs │ │ └── pool.rs │ ├── device_manager │ │ ├── device.rs │ │ ├── io.rs │ │ ├── manager.rs │ │ ├── mod.rs │ │ ├── novacom.rs │ │ └── privkey.rs │ ├── error.rs │ ├── event_channel │ │ ├── channel.rs │ │ └── mod.rs │ ├── lib.rs │ ├── main.rs │ ├── plugins │ │ ├── cmd.rs │ │ ├── device.rs │ │ ├── devmode.rs │ │ ├── file.rs │ │ ├── local_file.rs │ │ ├── mod.rs │ │ └── shell.rs │ ├── remote_files │ │ ├── mod.rs │ │ ├── serve.rs │ │ └── sftp.rs │ ├── session_manager │ │ ├── manager.rs │ │ ├── mod.rs │ │ └── proc.rs │ ├── shell_manager │ │ ├── manager.rs │ │ ├── mod.rs │ │ ├── shell.rs │ │ └── token.rs │ ├── spawn_manager │ │ ├── manager.rs │ │ └── mod.rs │ └── tests │ │ ├── common │ │ ├── mod.rs │ │ └── test_server │ │ │ ├── entrypoint.d │ │ │ └── setpasswd.sh │ │ │ ├── keys │ │ │ ├── id_root │ │ │ └── id_root.pub │ │ │ └── mod.rs │ │ └── mod.rs ├── tauri.conf.json └── tauri.linux.conf.json ├── src ├── app │ ├── add-device │ │ ├── add-device.component.scss │ │ ├── add-device.module.ts │ │ ├── conn-hint │ │ │ ├── conn-hint.component.html │ │ │ ├── conn-hint.component.scss │ │ │ └── conn-hint.component.ts │ │ ├── device-editor │ │ │ ├── device-editor.component.html │ │ │ ├── device-editor.component.scss │ │ │ ├── device-editor.component.ts │ │ │ ├── devmode-passphrase-hint │ │ │ │ ├── devmode-passphrase-hint.component.html │ │ │ │ ├── devmode-passphrase-hint.component.scss │ │ │ │ └── devmode-passphrase-hint.component.ts │ │ │ ├── key-passphrase-prompt │ │ │ │ ├── key-passphrase-prompt.component.html │ │ │ │ ├── key-passphrase-prompt.component.scss │ │ │ │ └── key-passphrase-prompt.component.ts │ │ │ ├── ssh-auth-value.directive.spec.ts │ │ │ ├── ssh-auth-value.directive.ts │ │ │ ├── ssh-password-hint │ │ │ │ ├── ssh-password-hint.component.html │ │ │ │ ├── ssh-password-hint.component.scss │ │ │ │ └── ssh-password-hint.component.ts │ │ │ └── ssh-privkey-hint │ │ │ │ ├── ssh-privkey-hint.component.html │ │ │ │ ├── ssh-privkey-hint.component.scss │ │ │ │ └── ssh-privkey-hint.component.ts │ │ ├── keyserver-hint │ │ │ ├── keyserver-hint.component.html │ │ │ ├── keyserver-hint.component.scss │ │ │ └── keyserver-hint.component.ts │ │ ├── retry-failed │ │ │ ├── retry-failed.component.html │ │ │ ├── retry-failed.component.scss │ │ │ └── retry-failed.component.ts │ │ └── wizard │ │ │ ├── add-device │ │ │ ├── add-device.component.html │ │ │ ├── add-device.component.scss │ │ │ └── add-device.component.ts │ │ │ ├── devmode-setup │ │ │ ├── devmode-setup.component.html │ │ │ ├── devmode-setup.component.scss │ │ │ ├── devmode-setup.component.ts │ │ │ └── step-header │ │ │ │ ├── step-header.component.html │ │ │ │ ├── step-header.component.scss │ │ │ │ └── step-header.component.ts │ │ │ ├── mode-select │ │ │ ├── mode-select.component.html │ │ │ ├── mode-select.component.scss │ │ │ └── mode-select.component.ts │ │ │ ├── wizard.component.html │ │ │ ├── wizard.component.scss │ │ │ └── wizard.component.ts │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.module.ts │ ├── apps │ │ ├── apps-routing.module.ts │ │ ├── apps.component.html │ │ ├── apps.component.scss │ │ ├── apps.component.ts │ │ ├── apps.module.ts │ │ ├── channel │ │ │ ├── channel.component.html │ │ │ ├── channel.component.scss │ │ │ └── channel.component.ts │ │ ├── details │ │ │ ├── details.component.html │ │ │ ├── details.component.scss │ │ │ └── details.component.ts │ │ ├── hbchannel-remove │ │ │ ├── hbchannel-remove.component.html │ │ │ ├── hbchannel-remove.component.scss │ │ │ └── hbchannel-remove.component.ts │ │ ├── index.ts │ │ └── installed │ │ │ ├── installed.component.html │ │ │ ├── installed.component.scss │ │ │ └── installed.component.ts │ ├── core │ │ ├── core.module.ts │ │ ├── event-channel.ts │ │ └── services │ │ │ ├── app-manager.service.ts │ │ │ ├── apps-repo.service.ts │ │ │ ├── backend-client.ts │ │ │ ├── dev-mode.service.ts │ │ │ ├── device-manager.service.ts │ │ │ ├── file.session.ts │ │ │ ├── index.ts │ │ │ ├── local-file.service.ts │ │ │ ├── progress-callback.ts │ │ │ ├── remote-command.service.spec.ts │ │ │ ├── remote-command.service.ts │ │ │ ├── remote-file.service.ts │ │ │ ├── remote-log.service.ts │ │ │ ├── remote-luna.service.ts │ │ │ ├── remote-shell.service.ts │ │ │ └── update.service.ts │ ├── debug │ │ ├── crashes │ │ │ ├── crashes.component.html │ │ │ ├── crashes.component.scss │ │ │ ├── crashes.component.ts │ │ │ └── details │ │ │ │ ├── details.component.html │ │ │ │ ├── details.component.scss │ │ │ │ └── details.component.ts │ │ ├── debug-routing.module.ts │ │ ├── debug.component.html │ │ ├── debug.component.scss │ │ ├── debug.component.ts │ │ ├── debug.module.ts │ │ ├── dmesg │ │ │ ├── dmesg.component.html │ │ │ ├── dmesg.component.scss │ │ │ └── dmesg.component.ts │ │ ├── index.ts │ │ ├── log-reader │ │ │ ├── log-reader.component.html │ │ │ ├── log-reader.component.scss │ │ │ └── log-reader.component.ts │ │ ├── ls-monitor │ │ │ ├── details │ │ │ │ ├── details.component.html │ │ │ │ ├── details.component.scss │ │ │ │ └── details.component.ts │ │ │ ├── ls-monitor.component.html │ │ │ ├── ls-monitor.component.scss │ │ │ ├── ls-monitor.component.ts │ │ │ └── object-highlight.pipe.ts │ │ └── pmlog │ │ │ ├── control │ │ │ ├── control.component.html │ │ │ ├── control.component.scss │ │ │ └── control.component.ts │ │ │ ├── pmlog.component.html │ │ │ ├── pmlog.component.scss │ │ │ ├── pmlog.component.ts │ │ │ └── set-context │ │ │ ├── set-context.component.html │ │ │ ├── set-context.component.scss │ │ │ └── set-context.component.ts │ ├── devices │ │ ├── devices.component.html │ │ ├── devices.component.scss │ │ ├── devices.component.ts │ │ └── inline-editor │ │ │ ├── inline-editor.component.html │ │ │ ├── inline-editor.component.scss │ │ │ ├── inline-editor.component.spec.ts │ │ │ └── inline-editor.component.ts │ ├── files │ │ ├── attrs-permissions.pipe.ts │ │ ├── create-directory-message │ │ │ ├── create-directory-message.component.html │ │ │ ├── create-directory-message.component.scss │ │ │ └── create-directory-message.component.ts │ │ ├── files-routing.module.ts │ │ ├── files-table │ │ │ ├── files-table.component.html │ │ │ ├── files-table.component.scss │ │ │ └── files-table.component.ts │ │ ├── files.component.html │ │ ├── files.component.scss │ │ ├── files.component.ts │ │ ├── files.module.ts │ │ └── index.ts │ ├── home │ │ ├── device-chooser │ │ │ ├── device-chooser.component.html │ │ │ ├── device-chooser.component.scss │ │ │ └── device-chooser.component.ts │ │ ├── home.component.html │ │ ├── home.component.scss │ │ ├── home.component.ts │ │ └── nav-more │ │ │ ├── nav-more.component.html │ │ │ ├── nav-more.component.scss │ │ │ └── nav-more.component.ts │ ├── info │ │ ├── devmode-countdown.pipe.ts │ │ ├── index.ts │ │ ├── info-routing.module.ts │ │ ├── info.component.html │ │ ├── info.component.scss │ │ ├── info.component.ts │ │ ├── info.module.ts │ │ └── renew-script │ │ │ ├── renew-script.component.html │ │ │ ├── renew-script.component.scss │ │ │ ├── renew-script.component.ts │ │ │ └── renew-script.sh.ts │ ├── remove-device │ │ ├── remove-device.component.html │ │ ├── remove-device.component.scss │ │ └── remove-device.component.ts │ ├── shared │ │ ├── components │ │ │ ├── error-card │ │ │ │ ├── error-card.component.html │ │ │ │ ├── error-card.component.scss │ │ │ │ └── error-card.component.ts │ │ │ ├── loading-card │ │ │ │ ├── loading-card.component.html │ │ │ │ ├── loading-card.component.scss │ │ │ │ └── loading-card.component.ts │ │ │ ├── message-dialog │ │ │ │ ├── message-dialog.component.html │ │ │ │ ├── message-dialog.component.scss │ │ │ │ ├── message-dialog.component.ts │ │ │ │ └── message-trace │ │ │ │ │ ├── message-trace.component.html │ │ │ │ │ ├── message-trace.component.scss │ │ │ │ │ └── message-trace.component.ts │ │ │ ├── page-not-found │ │ │ │ ├── page-not-found.component.html │ │ │ │ ├── page-not-found.component.scss │ │ │ │ └── page-not-found.component.ts │ │ │ ├── progress-dialog │ │ │ │ ├── progress-dialog.component.html │ │ │ │ ├── progress-dialog.component.scss │ │ │ │ └── progress-dialog.component.ts │ │ │ ├── stat-storage-info │ │ │ │ ├── stat-storage-info.component.html │ │ │ │ ├── stat-storage-info.component.scss │ │ │ │ └── stat-storage-info.component.ts │ │ │ └── term-size-calculator │ │ │ │ ├── size-calculator.component.html │ │ │ │ ├── size-calculator.component.scss │ │ │ │ └── size-calculator.component.ts │ │ ├── constants.ts │ │ ├── directives │ │ │ ├── external-link.directive.ts │ │ │ ├── index.ts │ │ │ ├── search-bar.directive.spec.ts │ │ │ └── search-bar.directive.ts │ │ ├── operators.ts │ │ ├── pipes │ │ │ ├── filesize.pipe.ts │ │ │ └── trust-uri.pipe.ts │ │ ├── shared.module.ts │ │ └── xterm │ │ │ ├── config.ts │ │ │ └── web-links.ts │ ├── terminal │ │ ├── dumb │ │ │ ├── dumb.component.html │ │ │ ├── dumb.component.scss │ │ │ └── dumb.component.ts │ │ ├── index.ts │ │ ├── pty │ │ │ ├── pty.component.html │ │ │ ├── pty.component.scss │ │ │ └── pty.component.ts │ │ ├── terminal-routing.module.ts │ │ ├── terminal.component.html │ │ ├── terminal.component.scss │ │ ├── terminal.component.ts │ │ └── terminal.module.ts │ ├── types │ │ ├── device-manager.ts │ │ ├── device.ts │ │ ├── file-session.ts │ │ ├── index.ts │ │ └── luna-apis.ts │ └── update-details │ │ ├── update-details.component.html │ │ ├── update-details.component.scss │ │ └── update-details.component.ts ├── assets │ ├── .gitkeep │ ├── icons │ │ ├── electron.bmp │ │ ├── favicon.256x256.png │ │ ├── favicon.512x512.png │ │ ├── favicon.icns │ │ ├── favicon.ico │ │ └── favicon.png │ └── images │ │ ├── hint-devmode-passphrase.png │ │ └── hint-key-server.png ├── environments │ ├── environment.dev.ts │ ├── environment.prod.ts │ └── environment.ts ├── index.html ├── main.ts ├── polyfills-test.ts ├── polyfills.ts ├── proxy.conf.json ├── release.json ├── styles.scss ├── styles │ ├── app-item.scss │ ├── no-select.scss │ ├── overflow.scss │ ├── shared.scss │ └── terminal.scss └── typings.d.ts ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | indent_size = 4 13 | quote_type = single 14 | 15 | [*.spec.ts] 16 | indent_size = 2 17 | 18 | [*.{rs,py}] 19 | indent_size = 4 20 | 21 | [*.md] 22 | max_line_length = off 23 | trim_trailing_whitespace = false 24 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: mariotaku 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Request A New App 4 | url: https://github.com/webosbrew/apps-repo/issues/new/choose 5 | about: For requests and suggestions for new apps, create issue in webosbrew/apps-repo 6 | - name: Report Issue of An App 7 | url: https://repo.webosbrew.org/ 8 | about: For reporting issues of an app, visit the app's page in webosbrew/apps-repo, and follow the link to the app's issue tracker 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest a new feature or improvement 3 | type: Feature 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Description 8 | description: Describe the feature you would like to have added. 9 | placeholder: "Example: I would like to add multiple apps repositories." 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: Motivation 15 | description: Why do you think this feature should be added? 16 | placeholder: "Example: I have multiple apps repositories and I want to see all the apps in one place." 17 | validations: 18 | required: true 19 | - type: textarea 20 | attributes: 21 | label: Additional Context 22 | description: Add any other context or screenshots about the feature request here. 23 | placeholder: "Example: My other repositories are..." 24 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 30 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 15 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.github/workflows/release-apk.yml: -------------------------------------------------------------------------------- 1 | name: 'Release APK' 2 | 3 | on: 4 | repository_dispatch: 5 | types: [ release-apk ] 6 | workflow_dispatch: 7 | inputs: 8 | version: 9 | description: 'Version to fetch from Google Play' 10 | required: true 11 | 12 | jobs: 13 | upload-signed-apk: 14 | name: Upload Signed APK 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Extract Version 20 | id: pkg-version 21 | uses: saionaro/extract-package-version@v1.3.0 22 | 23 | - name: Setup Ruby 24 | uses: ruby/setup-ruby@v1 25 | with: 26 | ruby-version: 3.3 27 | bundler-cache: 'true' 28 | 29 | - name: Download APK 30 | id: download-apk 31 | uses: maierj/fastlane-action@v3.1.0 32 | with: 33 | lane: download_apk 34 | verbose: ${{ runner.debug }} 35 | env: 36 | GOOGLE_JSON_KEY_DATA: ${{ secrets.GOOGLE_JSON_KEY_DATA }} 37 | 38 | - name: Create Release 39 | uses: ncipollo/release-action@v1 40 | with: 41 | name: Dev Manager ${{ steps.pkg-version.outputs.version }} 42 | tag: v${{ steps.pkg-version.outputs.version }} 43 | allowUpdates: true 44 | omitNameDuringUpdate: true 45 | omitBodyDuringUpdate: true 46 | omitPrereleaseDuringUpdate: true 47 | artifacts: fastlane/downloads/*.apk 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | # backup files 3 | *~ 4 | *.bak 5 | 6 | # compiled output 7 | /dist 8 | /tmp 9 | /out-tsc 10 | /app-builds 11 | /release 12 | /target 13 | index.js 14 | preload.js 15 | src/**/*.js 16 | !src/karma.conf.js 17 | *.js.map 18 | .angular/ 19 | .nx/ 20 | 21 | # dependencies 22 | /node_modules 23 | 24 | # IDEs and editors 25 | /.idea 26 | .project 27 | .classpath 28 | .c9/ 29 | *.launch 30 | .settings/ 31 | *.sublime-workspace 32 | 33 | # IDE - VSCode 34 | .vscode/* 35 | .vscode/settings.json 36 | !.vscode/tasks.json 37 | !.vscode/launch.json 38 | !.vscode/extensions.json 39 | 40 | # misc 41 | /.sass-cache 42 | /connect.lock 43 | /coverage 44 | /libpeerconnection.log 45 | npm-debug.log 46 | testem.log 47 | /typings 48 | 49 | # e2e 50 | /e2e/*.js 51 | !/e2e/protractor.conf.js 52 | /e2e/*.map 53 | 54 | # System Files 55 | .DS_Store 56 | Thumbs.db 57 | 58 | local.properties 59 | 60 | ### Fastlane 61 | 62 | # fastlane specific 63 | **/fastlane/report.xml 64 | 65 | # deliver temporary files 66 | **/fastlane/Preview.html 67 | 68 | # snapshot generated screenshots 69 | **/fastlane/screenshots 70 | 71 | # scan temporary files 72 | **/fastlane/test_output 73 | 74 | .secrets 75 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save=true 2 | save-exact=true 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "src-tauri" 5 | ] 6 | 7 | [patch.crates-io] 8 | r2d2 = { git = "https://github.com/mariotaku/r2d2.git", rev = "aaa2a2b" } 9 | tauri = { git = "https://github.com/mariotaku/tauri.git", rev = "f501f7b" } 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | 5 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 6 | eval_gemfile(plugins_path) if File.exist?(plugins_path) 7 | -------------------------------------------------------------------------------- /fastlane/.gitignore: -------------------------------------------------------------------------------- 1 | /conf 2 | /metadata 3 | /downloads 4 | .env 5 | -------------------------------------------------------------------------------- /fastlane/Appfile: -------------------------------------------------------------------------------- 1 | json_key_file ENV['GOOGLE_JSON_KEY_FILE'] if ENV['GOOGLE_JSON_KEY_FILE'] 2 | json_key_data_raw ENV['GOOGLE_JSON_KEY_DATA'] if ENV['GOOGLE_JSON_KEY_DATA'] 3 | package_name "org.webosbrew.devman" 4 | -------------------------------------------------------------------------------- /fastlane/Pluginfile: -------------------------------------------------------------------------------- 1 | # Autogenerated by fastlane 2 | # 3 | # Ensure this file is checked in to source control! 4 | 5 | gem 'fastlane-plugin-download_file', :github => 'ricca509/fastlane-plugin-download_file', :ref => 'cb2aa03' 6 | gem 'fastlane-plugin-unzip' 7 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ---- 3 | 4 | # Installation 5 | 6 | Make sure you have the latest version of the Xcode command line tools installed: 7 | 8 | ```sh 9 | xcode-select --install 10 | ``` 11 | 12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) 13 | 14 | # Available Actions 15 | 16 | ## Android 17 | 18 | ### android build 19 | 20 | ```sh 21 | [bundle exec] fastlane android build 22 | ``` 23 | 24 | Build apk or aab 25 | 26 | ### android deploy 27 | 28 | ```sh 29 | [bundle exec] fastlane android deploy 30 | ``` 31 | 32 | Deploy a new version to the Google Play 33 | 34 | ### android download_apk 35 | 36 | ```sh 37 | [bundle exec] fastlane android download_apk 38 | ``` 39 | 40 | Download the signed universal APK from the Google Play 41 | 42 | ---- 43 | 44 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. 45 | 46 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). 47 | 48 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 49 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | /** @param config {import('karma').Config} */ 5 | module.exports = function (config) { 6 | config.set({ 7 | basePath: '', 8 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 9 | plugins: [ 10 | require('karma-jasmine'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma'), 14 | require('./scripts/karma-tauri-launcher'), 15 | ], 16 | client: { 17 | jasmine: { 18 | // you can add configuration options for Jasmine here 19 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 20 | // for example, you can disable the random execution with `random: false` 21 | // or set a specific seed with `seed: 4321` 22 | }, 23 | clearContext: false // leave Jasmine Spec Runner output visible in browser 24 | }, 25 | captureTimeout: 3600 * 1000, 26 | jasmineHtmlReporter: { 27 | suppressAll: true // removes the duplicated traces 28 | }, 29 | coverageReporter: { 30 | dir: require('path').join(__dirname, './coverage/ui'), 31 | subdir: '.', 32 | reporters: [ 33 | {type: 'html'}, 34 | {type: 'text-summary'} 35 | ] 36 | }, 37 | reporters: ['progress', 'kjhtml'], 38 | retryLimit: 0, 39 | port: 9876, 40 | colors: true, 41 | logLevel: config.LOG_INFO, 42 | autoWatch: true, 43 | singleRun: false, 44 | restartOnFileChange: true 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /resources/icon/android/main/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /resources/icon/android/main/mipmap/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /resources/icon/mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/resources/icon/mobile.png -------------------------------------------------------------------------------- /scripts/karma-tauri-launcher.js: -------------------------------------------------------------------------------- 1 | const TauriLauncher = /** @class */ (function () { 2 | 3 | function TauriLauncher(baseBrowserDecorator, name, logger) { 4 | baseBrowserDecorator(this); 5 | let tauriCommand = ['dev']; 6 | if (name === 'TauriAndroid') { 7 | tauriCommand = ['android', 'dev']; 8 | } 9 | this._getOptions = function (url) { 10 | const tauriConfOverride = { 11 | build: { 12 | beforeDevCommand: 'adb reverse tcp:9876 tcp:9876', 13 | devUrl: url 14 | }, 15 | app: { 16 | windows: [ 17 | { 18 | url: url 19 | } 20 | ] 21 | } 22 | }; 23 | return ['scripts/tauri-wrapper.js', ...tauriCommand, '-c', JSON.stringify(tauriConfOverride), '-f', 'karma']; 24 | }; 25 | let log = logger.create('tauri'); 26 | this._onStdout = function (data) { 27 | log.debug(data.toString().trimEnd()); 28 | }; 29 | this._onStderr = function (data) { 30 | log.debug(data.toString().trimEnd()); 31 | }; 32 | } 33 | 34 | TauriLauncher.prototype = { 35 | name: 'Tauri', 36 | DEFAULT_CMD: new Proxy({}, { 37 | get: () => process.execPath, 38 | }), 39 | }; 40 | 41 | TauriLauncher.$inject = ['baseBrowserDecorator', 'name', 'logger']; 42 | 43 | return TauriLauncher; 44 | }()); 45 | 46 | module.exports = { 47 | 'launcher:TauriDesktop': ['type', TauriLauncher], 48 | 'launcher:TauriAndroid': ['type', TauriLauncher], 49 | }; 50 | -------------------------------------------------------------------------------- /scripts/ng-wrapper.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | 5 | /** 6 | * @param arg {String} 7 | * @return {boolean} 8 | */ 9 | function isConfigurationArgument(arg) { 10 | return arg.startsWith('--configuration='); 11 | } 12 | 13 | switch (process.argv[2]) { 14 | case 'serve': 15 | if (process.env.TAURI_DEV_HOST) { 16 | process.argv.push(`--host=${process.env.TAURI_DEV_HOST}`, '--disable-host-check'); 17 | } 18 | // noinspection FallThroughInSwitchStatementJS 19 | case 'build': 20 | if (process.env.TAURI_DEBUG === "true" && !process.argv.slice(3).find(isConfigurationArgument)) { 21 | process.argv.push('--configuration=development'); 22 | } 23 | break; 24 | } 25 | 26 | require('@angular/cli/bin/ng'); 27 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | /gen/ 5 | .idea/ 6 | /.cargo/ 7 | -------------------------------------------------------------------------------- /src-tauri/README.md: -------------------------------------------------------------------------------- 1 | ## Build on Windows 2 | 3 | ### Install Dependencies 4 | 5 | ```vcpkg install libssh:x64-windows openssl:x64-windows``` 6 | -------------------------------------------------------------------------------- /src-tauri/assets/app.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Version=1.0 4 | Name=webOS Dev Manager 5 | {{#if comment}} 6 | Comment={{comment}} 7 | {{/if}} 8 | Exec={{exec}} 9 | Icon={{icon}} 10 | Terminal=false 11 | Categories={{categories}} 12 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | use tauri_build::{Attributes, CodegenContext, InlinedPlugin}; 2 | 3 | fn main() { 4 | tauri_build::try_build( 5 | Attributes::new() 6 | .codegen(CodegenContext::new()) 7 | .plugin( 8 | "device-manager", 9 | InlinedPlugin::new().commands(&[ 10 | "list", 11 | "set_default", 12 | "add", 13 | "remove", 14 | "novacom_getkey", 15 | "localkey_verify", 16 | "privkey_read", 17 | "check_connection", 18 | "app_ssh_key_path", 19 | "app_ssh_pubkey", 20 | "ssh_key_dir", 21 | ]), 22 | ) 23 | .plugin( 24 | "remote-command", 25 | InlinedPlugin::new().commands(&["exec", "spawn"]), 26 | ) 27 | .plugin( 28 | "remote-shell", 29 | InlinedPlugin::new() 30 | .commands(&["open", "close", "write", "resize", "screen", "list"]), 31 | ) 32 | .plugin( 33 | "remote-file", 34 | InlinedPlugin::new() 35 | .commands(&["ls", "read", "write", "get", "put", "get_temp", "serve"]), 36 | ) 37 | .plugin( 38 | "dev-mode", 39 | InlinedPlugin::new().commands(&["status", "token"]), 40 | ) 41 | .plugin( 42 | "local-file", 43 | InlinedPlugin::new().commands(&["checksum", "remove", "copy", "temp_path"]), 44 | ), 45 | ) 46 | .expect("failed to run tauri-build"); 47 | } 48 | -------------------------------------------------------------------------------- /src-tauri/capabilities/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/capabilities.json", 3 | "identifier": "app-caps", 4 | "windows": [ 5 | "main" 6 | ], 7 | "permissions": [ 8 | "core:event:default", 9 | "core:webview:default", 10 | "core:path:default", 11 | "core:path:allow-resolve-directory", 12 | "fs:default", 13 | "fs:write-all", 14 | "shell:allow-open", 15 | "dialog:allow-open", 16 | "dialog:allow-save", 17 | "upload:allow-download", 18 | "device-manager:default", 19 | "remote-command:default", 20 | "remote-shell:default", 21 | "remote-file:default", 22 | "dev-mode:default", 23 | "local-file:default", 24 | "log:default", 25 | "os:default", 26 | { 27 | "identifier": "http:default", 28 | "allow": [ 29 | { 30 | "url": "https://*" 31 | } 32 | ] 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/ios/AppIcon-20x20@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/ios/AppIcon-20x20@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/ios/AppIcon-20x20@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/ios/AppIcon-20x20@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/ios/AppIcon-29x29@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/ios/AppIcon-29x29@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/ios/AppIcon-29x29@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/ios/AppIcon-29x29@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/ios/AppIcon-40x40@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/ios/AppIcon-40x40@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/ios/AppIcon-40x40@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/ios/AppIcon-40x40@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/ios/AppIcon-512@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/ios/AppIcon-60x60@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/ios/AppIcon-60x60@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/ios/AppIcon-76x76@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/ios/AppIcon-76x76@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png -------------------------------------------------------------------------------- /src-tauri/permissions/dev-mode/default.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | description = "Default permissions for the plugin" 3 | permissions = [ 4 | "allow-status", 5 | "allow-token" 6 | ] 7 | -------------------------------------------------------------------------------- /src-tauri/permissions/device-manager/default.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | description = "Default permissions for the plugin" 3 | permissions = [ 4 | "allow-list", 5 | "allow-set-default", 6 | "allow-add", 7 | "allow-remove", 8 | "allow-novacom-getkey", 9 | "allow-localkey-verify", 10 | "allow-privkey-read", 11 | "allow-check-connection", 12 | "allow-app-ssh-key-path", 13 | "allow-app-ssh-pubkey", 14 | "allow-ssh-key-dir", 15 | ] 16 | -------------------------------------------------------------------------------- /src-tauri/permissions/local-file/default.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | description = "Default permissions for the plugin" 3 | permissions = [ 4 | "allow-checksum", 5 | "allow-remove", 6 | "allow-copy", 7 | "allow-temp-path", 8 | ] 9 | -------------------------------------------------------------------------------- /src-tauri/permissions/remote-command/default.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | description = "Default permissions for the plugin" 3 | permissions = [ 4 | "allow-exec", 5 | "allow-spawn" 6 | ] 7 | -------------------------------------------------------------------------------- /src-tauri/permissions/remote-file/default.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | description = "Default permissions for the plugin" 3 | permissions = [ 4 | "allow-ls", 5 | "allow-read", 6 | "allow-write", 7 | "allow-get", 8 | "allow-put", 9 | "allow-get-temp", 10 | "allow-serve" 11 | ] 12 | -------------------------------------------------------------------------------- /src-tauri/permissions/remote-shell/default.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | description = "Default permissions for the plugin" 3 | permissions = [ 4 | "allow-open", 5 | "allow-close", 6 | "allow-write", 7 | "allow-resize", 8 | "allow-screen", 9 | "allow-list" 10 | ] 11 | -------------------------------------------------------------------------------- /src-tauri/src/app_dirs/mod.rs: -------------------------------------------------------------------------------- 1 | use ssh_key::private::{Ed25519Keypair, KeypairData}; 2 | use ssh_key::{rand_core::OsRng, LineEnding, PrivateKey}; 3 | use std::fs::create_dir_all; 4 | use std::path::PathBuf; 5 | 6 | use crate::error::Error; 7 | 8 | pub trait GetSshDir { 9 | fn get_ssh_dir(&self) -> Option; 10 | 11 | fn ensure_ssh_dir(&self) -> Result { 12 | let Some(dir) = self.get_ssh_dir() else { 13 | return Err(Error::bad_config()); 14 | }; 15 | if !dir.exists() { 16 | create_dir_all(&dir)?; 17 | } 18 | Ok(dir) 19 | } 20 | } 21 | 22 | pub trait SetSshDir { 23 | fn set_ssh_dir(&self, dir: PathBuf); 24 | } 25 | 26 | pub trait GetConfDir { 27 | fn get_conf_dir(&self) -> Option; 28 | fn ensure_conf_dir(&self) -> Result { 29 | let Some(dir) = self.get_conf_dir() else { 30 | return Err(Error::bad_config()); 31 | }; 32 | if !dir.exists() { 33 | create_dir_all(&dir)?; 34 | } 35 | Ok(dir) 36 | } 37 | } 38 | 39 | pub trait SetConfDir { 40 | fn set_conf_dir(&self, dir: PathBuf); 41 | } 42 | 43 | pub trait GetAppSshKeyDir { 44 | fn get_app_ssh_key_path(&self) -> Result; 45 | 46 | fn get_app_ssh_pubkey(&self) -> Result; 47 | 48 | fn ensure_app_ssh_key_path(&self) -> Result { 49 | let path = self.get_app_ssh_key_path()?; 50 | if !path.exists() || !PrivateKey::read_openssh_file(&path).is_ok() { 51 | let keypair = Ed25519Keypair::random(&mut OsRng); 52 | let key_comment = String::from(&format!("devman_{:x}", keypair.public)[0..15]); 53 | log::info!( 54 | "Generating new SSH key `{}` and saving to {}", 55 | key_comment, 56 | path.display() 57 | ); 58 | let key_data = KeypairData::Ed25519(keypair); 59 | PrivateKey::new(key_data, key_comment) 60 | .unwrap() 61 | .write_openssh_file(&path, LineEnding::LF) 62 | .map_err(|e| Error::BadPrivateKey { 63 | message: format!("{:?}", e), 64 | })?; 65 | } 66 | Ok(path) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src-tauri/src/byte_string.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)] 4 | pub enum Encoding { 5 | #[serde(rename = "binary")] 6 | Binary, 7 | #[serde(rename = "string")] 8 | String, 9 | } 10 | 11 | #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] 12 | #[serde(untagged)] 13 | pub enum ByteString { 14 | Binary(Vec), 15 | String(String), 16 | } 17 | 18 | impl ByteString { 19 | pub fn parse(raw: &[u8], encoding: Encoding) -> Result { 20 | match encoding { 21 | Encoding::Binary => Ok(ByteString::Binary(raw.to_vec())), 22 | Encoding::String => { 23 | let string = std::str::from_utf8(raw)?; 24 | Ok(ByteString::String(string.to_string())) 25 | } 26 | } 27 | } 28 | } 29 | 30 | impl AsRef<[u8]> for ByteString { 31 | fn as_ref(&self) -> &[u8] { 32 | match self { 33 | ByteString::Binary(bytes) => bytes, 34 | ByteString::String(string) => string.as_bytes(), 35 | } 36 | } 37 | } 38 | 39 | #[cfg(test)] 40 | mod tests { 41 | use crate::byte_string::ByteString; 42 | use serde_json::Value; 43 | 44 | #[test] 45 | fn test_serializing() { 46 | let bytes = ByteString::Binary(vec![1, 2, 3]); 47 | let string = ByteString::String("hello".to_string()); 48 | let bytes_ser = serde_json::to_value(&bytes).unwrap(); 49 | let string_ser = serde_json::to_value(&string).unwrap(); 50 | assert_eq!( 51 | bytes_ser, 52 | Value::Array(vec![ 53 | Value::Number(1.into()), 54 | Value::Number(2.into()), 55 | Value::Number(3.into()) 56 | ]) 57 | ); 58 | assert_eq!(string_ser, Value::String("hello".to_string())); 59 | } 60 | 61 | #[test] 62 | fn test_deserializing() { 63 | let bytes = ByteString::Binary(vec![1, 2, 3]); 64 | let string = ByteString::String("hello".to_string()); 65 | let bytes_ser = serde_json::to_string(&bytes).unwrap(); 66 | let string_ser = serde_json::to_string(&string).unwrap(); 67 | let bytes_de: ByteString = serde_json::from_str("[1,2,3]").unwrap(); 68 | let string_de: ByteString = serde_json::from_str("\"hello\"").unwrap(); 69 | assert_eq!(bytes_de, bytes); 70 | assert_eq!(string_de, string); 71 | assert_eq!(bytes_ser, "[1,2,3]"); 72 | assert_eq!(string_ser, "\"hello\""); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src-tauri/src/conn_pool/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::device_manager::Device; 2 | use crate::error::Error; 3 | use libssh_rs::Session; 4 | use r2d2::{Pool, PooledConnection}; 5 | use std::path::PathBuf; 6 | use std::sync::{Arc, Mutex}; 7 | use uuid::Uuid; 8 | 9 | pub mod connection; 10 | pub mod pool; 11 | mod cmd; 12 | 13 | pub struct DeviceConnection { 14 | id: Uuid, 15 | pub device: Device, 16 | pub user: Option, 17 | session: Session, 18 | last_ok: Mutex, 19 | } 20 | 21 | #[derive(Debug)] 22 | pub struct DeviceConnectionUserInfo { 23 | pub uid: Id, 24 | pub gid: Id, 25 | pub groups: Vec, 26 | } 27 | 28 | pub struct Id { 29 | pub id: u32, 30 | pub name: Option, 31 | } 32 | 33 | pub type ManagedDeviceConnection = PooledConnection; 34 | 35 | pub struct DeviceConnectionPool { 36 | inner: Pool, 37 | last_error: Arc>>, 38 | } 39 | 40 | pub struct DeviceConnectionManager { 41 | device: Device, 42 | ssh_dir: Option, 43 | } 44 | 45 | pub use cmd::ExecuteCommand; 46 | -------------------------------------------------------------------------------- /src-tauri/src/device_manager/device.rs: -------------------------------------------------------------------------------- 1 | use crate::device_manager::Device; 2 | 3 | impl Device { 4 | pub(crate) fn valid_passphrase(&self) -> Option { 5 | self.passphrase.clone().filter(|s| !s.is_empty()) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src-tauri/src/device_manager/novacom.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use httparse::{Response, Status}; 3 | use std::io::{Error as IoError, Read, Write}; 4 | use std::net::{TcpStream, ToSocketAddrs}; 5 | use std::time::Duration; 6 | 7 | pub(crate) fn fetch_key(host: &str, port: u16) -> Result { 8 | let address = format!("{host}:{port}") 9 | .to_socket_addrs()? 10 | .next() 11 | .ok_or(Error::NotFound)?; 12 | let mut stream = TcpStream::connect_timeout(&address, Duration::from_secs(10))?; 13 | stream.write(b"GET /webos_rsa HTTP/1.0\r\n")?; 14 | stream.write(b"Connection: close\r\n")?; 15 | stream.write(b"\r\n")?; 16 | 17 | let mut buffer = [0u8; 65536]; 18 | let buffer_size = stream.read(&mut buffer)?; 19 | let mut headers = [httparse::EMPTY_HEADER; 64]; 20 | let mut response = Response::new(&mut headers); 21 | let Status::Complete(size_to_skip) = response 22 | .parse(&buffer[..buffer_size]) 23 | .map_err(|e| IoError::new(std::io::ErrorKind::InvalidData, e))? 24 | else { 25 | return Err(Error::NotFound); 26 | }; 27 | if response.code.unwrap() != 200 { 28 | return Err(Error::NotFound); 29 | } 30 | Ok(String::from_utf8_lossy(&buffer[size_to_skip..buffer_size]).to_string()) 31 | } 32 | 33 | #[cfg(test)] 34 | mod tests { 35 | use super::*; 36 | use httptest::matchers::request; 37 | use httptest::responders::status_code; 38 | use httptest::{Expectation, Server}; 39 | use std::io::ErrorKind; 40 | 41 | #[test] 42 | fn fetch_key_404() { 43 | let server = Server::run(); 44 | server.expect( 45 | Expectation::matching(request::method_path("GET", "/webos_rsa")) 46 | .respond_with(status_code(404)), 47 | ); 48 | let addr = server.addr(); 49 | let result = fetch_key(addr.ip().to_string().as_str(), addr.port()); 50 | assert!(result.is_err()); 51 | } 52 | 53 | #[test] 54 | fn fetch_key_refused() { 55 | let result = fetch_key("127.0.0.1", 9991); 56 | assert!(result.is_err()); 57 | assert_eq!( 58 | match result.unwrap_err() { 59 | Error::IO { code, .. } => code, 60 | _ => ErrorKind::Other, 61 | }, 62 | ErrorKind::ConnectionRefused 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src-tauri/src/device_manager/privkey.rs: -------------------------------------------------------------------------------- 1 | use std::io::{ErrorKind, Read}; 2 | use std::path::Path; 3 | 4 | use libssh_rs::{PublicKeyHashType, SshKey}; 5 | 6 | use crate::device_manager::PrivateKey; 7 | use crate::error::Error; 8 | 9 | impl PrivateKey { 10 | pub fn content(&self, ssh_dir: Option<&Path>) -> Result { 11 | match self { 12 | PrivateKey::Path { name } => { 13 | let mut ssh_dir = ssh_dir.ok_or(Error::bad_config())?; 14 | if cfg!(mobile) { 15 | let ssh_parent = ssh_dir.parent().ok_or(Error::bad_config())?; 16 | if ssh_parent.join(name).is_file() { 17 | ssh_dir = ssh_parent; 18 | } 19 | } 20 | let mut secret_file = 21 | std::fs::File::open(ssh_dir.join(name)).map_err(|err| match err.kind() { 22 | ErrorKind::NotFound => Error::BadPrivateKey { 23 | message: format!("Private key file not found: {}", name), 24 | }, 25 | _ => err.into(), 26 | })?; 27 | let mut secret = String::new(); 28 | secret_file.read_to_string(&mut secret)?; 29 | Ok(secret) 30 | } 31 | PrivateKey::Data { data } => Ok(data.clone()), 32 | } 33 | } 34 | 35 | pub fn name(&self, passphrase: Option) -> Result { 36 | match self { 37 | PrivateKey::Path { name } => Ok(name.clone()), 38 | PrivateKey::Data { data } => Ok(String::from( 39 | &hex::encode( 40 | SshKey::from_privkey_base64(data, passphrase.as_deref())? 41 | .get_public_key_hash(PublicKeyHashType::Sha256)?, 42 | )[..10], 43 | )), 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src-tauri/src/event_channel/channel.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | 3 | use serde::Serialize; 4 | use tauri::{AppHandle, Emitter, Listener, Runtime}; 5 | use uuid::Uuid; 6 | 7 | use crate::event_channel::{EventChannel, EventHandler}; 8 | 9 | impl EventChannel 10 | where 11 | R: Runtime, 12 | H: EventHandler + Sync + Send + 'static, 13 | { 14 | pub fn rx(&self, data: D) 15 | where 16 | D: Serialize + Clone, 17 | { 18 | self.app 19 | .emit( 20 | &format!("event_channel:{}:{}:rx", self.category, self.id), 21 | data, 22 | ) 23 | .unwrap(); 24 | } 25 | 26 | pub fn closed(&self, data: D) 27 | where 28 | D: Serialize + Clone, 29 | { 30 | self.app 31 | .emit( 32 | &format!("event_channel:{}:{}:closed", self.category, self.id), 33 | data, 34 | ) 35 | .unwrap(); 36 | } 37 | 38 | pub fn listen(&self, handler: H) { 39 | let handler = Arc::new(handler); 40 | *self.handler.lock().unwrap() = Some(handler.clone()); 41 | let handler2 = handler.clone(); 42 | let handler3 = handler.clone(); 43 | self.app.once( 44 | format!("event_channel:{}:{}:close", self.category, self.id), 45 | move |e| { 46 | handler2.close(Some(e.payload())); 47 | }, 48 | ); 49 | self.listeners.lock().unwrap().push(self.app.listen( 50 | format!("event_channel:{}:{}:tx", self.category, self.id), 51 | move |e| { 52 | handler3.tx(Some(e.payload())); 53 | }, 54 | )); 55 | } 56 | 57 | pub fn token(&self) -> String { 58 | return format!("event_channel:{}:{}", self.category, self.id); 59 | } 60 | 61 | pub fn new(app: AppHandle, category: S) -> EventChannel 62 | where 63 | S: Into, 64 | { 65 | return EventChannel { 66 | app, 67 | category: category.into(), 68 | id: Uuid::new_v4(), 69 | handler: Mutex::default(), 70 | listeners: Mutex::default(), 71 | }; 72 | } 73 | } 74 | 75 | impl Drop for EventChannel 76 | where 77 | R: Runtime, 78 | H: EventHandler + Send + 'static, 79 | { 80 | fn drop(&mut self) { 81 | for listener in self.listeners.lock().unwrap().drain(..) { 82 | self.app.unlisten(listener); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src-tauri/src/event_channel/mod.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | use tauri::{AppHandle, EventId, Runtime}; 3 | use uuid::Uuid; 4 | 5 | mod channel; 6 | 7 | pub struct EventChannel { 8 | app: AppHandle, 9 | category: String, 10 | id: Uuid, 11 | pub handler: Mutex>>, 12 | listeners: Mutex>, 13 | } 14 | 15 | pub trait EventHandler: Sized { 16 | fn tx(&self, payload: Option<&str>); 17 | //noinspection RsLiveness 18 | fn close(&self, _payload: Option<&str>) { 19 | unimplemented!(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr( 2 | all(not(debug_assertions), target_os = "windows"), 3 | windows_subsystem = "windows" 4 | )] 5 | 6 | fn main() { 7 | #[cfg(feature = "desktop")] 8 | { 9 | use log::LevelFilter; 10 | env_logger::builder() 11 | .filter_level(LevelFilter::Debug) 12 | .init(); 13 | } 14 | devman::run(); 15 | } 16 | -------------------------------------------------------------------------------- /src-tauri/src/plugins/local_file.rs: -------------------------------------------------------------------------------- 1 | use sha2::{Digest, Sha256}; 2 | use std::env::temp_dir; 3 | use tauri::ipc::Channel; 4 | use tauri::plugin::{Builder, TauriPlugin}; 5 | use tauri::{AppHandle, Manager, Runtime}; 6 | use tauri_plugin_fs::{FilePath, Fs, OpenOptions}; 7 | use uuid::Uuid; 8 | 9 | use crate::error::Error; 10 | use crate::plugins::file; 11 | use crate::plugins::file::CopyProgress; 12 | 13 | #[tauri::command] 14 | async fn checksum( 15 | app: AppHandle, 16 | path: FilePath, 17 | algorithm: String, 18 | ) -> Result { 19 | let mut hasher = match algorithm.as_str() { 20 | "sha256" => Sha256::new(), 21 | _ => return Err(Error::Unsupported), 22 | }; 23 | let fs = app.state::>(); 24 | let mut opt = OpenOptions::new(); 25 | opt.read(true); 26 | let mut file = fs.open(path, opt)?; 27 | std::io::copy(&mut file, &mut hasher)?; 28 | let result = hasher.finalize(); 29 | Ok(hex::encode(result)) 30 | } 31 | 32 | #[tauri::command] 33 | async fn remove(path: String, recursive: bool) -> Result<(), Error> { 34 | if recursive { 35 | tokio::fs::remove_dir_all(&path).await?; 36 | } else { 37 | tokio::fs::remove_file(&path).await?; 38 | } 39 | Ok(()) 40 | } 41 | 42 | #[tauri::command] 43 | async fn temp_path(extension: String) -> Result { 44 | let temp_path = temp_dir().join(format!("webos-dev-tmp-{}{}", Uuid::new_v4(), extension)); 45 | temp_path 46 | .to_str() 47 | .map(|s| String::from(s)) 48 | .ok_or_else(|| Error::new(&format!("Bad temp_path {:?}", temp_path))) 49 | } 50 | 51 | #[tauri::command] 52 | fn copy( 53 | app: AppHandle, 54 | source: FilePath, 55 | target: FilePath, 56 | on_progress: Channel, 57 | ) -> Result<(), Error> { 58 | let fs = app.state::>(); 59 | let mut read_options = OpenOptions::new(); 60 | read_options.read(true); 61 | let mut src_file = fs.open(source, read_options)?; 62 | let src_len = src_file.metadata()?.len() as usize; 63 | let mut write_options = OpenOptions::new(); 64 | write_options.write(true).create(true); 65 | let mut dest_file = fs.open(target, write_options)?; 66 | file::copy(&mut src_file, &mut dest_file, src_len, &on_progress)?; 67 | Ok(()) 68 | } 69 | 70 | pub fn plugin(name: &'static str) -> TauriPlugin { 71 | Builder::new(name) 72 | .invoke_handler(tauri::generate_handler![checksum, remove, copy, temp_path]) 73 | .build() 74 | } 75 | -------------------------------------------------------------------------------- /src-tauri/src/plugins/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cmd; 2 | pub mod device; 3 | pub mod devmode; 4 | pub mod file; 5 | pub mod local_file; 6 | pub mod shell; 7 | -------------------------------------------------------------------------------- /src-tauri/src/remote_files/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | pub(crate) mod serve; 4 | mod sftp; 5 | 6 | #[derive(Serialize, Clone, Debug)] 7 | pub struct FileItem { 8 | filename: String, 9 | r#type: String, 10 | mode: String, 11 | user: Option, 12 | group: Option, 13 | size: usize, 14 | mtime: f64, 15 | link: Option, 16 | access: Option, 17 | } 18 | 19 | #[derive(Serialize, Deserialize, Clone, Debug)] 20 | pub struct LinkInfo { 21 | target: Option, 22 | broken: Option, 23 | } 24 | 25 | #[derive(Serialize, Clone, Debug)] 26 | pub struct PermInfo { 27 | read: bool, 28 | write: bool, 29 | execute: bool, 30 | } 31 | -------------------------------------------------------------------------------- /src-tauri/src/session_manager/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::path::PathBuf; 3 | use std::sync::mpsc::Sender; 4 | use std::sync::{Arc, Condvar, Mutex}; 5 | 6 | use serde::Serialize; 7 | 8 | use crate::conn_pool::DeviceConnectionPool; 9 | use crate::device_manager::Device; 10 | 11 | mod manager; 12 | mod proc; 13 | 14 | #[derive(Default)] 15 | pub struct SessionManager { 16 | ssh_dir: Mutex>, 17 | pools: Mutex>, 18 | } 19 | 20 | pub struct Proc { 21 | pub(crate) device: Device, 22 | pub(crate) command: String, 23 | pub(crate) callback: Mutex>>, 24 | pub(crate) ready: Arc<(Mutex, Condvar)>, 25 | pub(crate) sender: Mutex>>>, 26 | pub(crate) interrupted: Mutex, 27 | } 28 | 29 | #[derive(Clone, Serialize)] 30 | pub struct ProcData { 31 | pub fd: u32, 32 | pub data: Vec, 33 | } 34 | 35 | #[derive(Debug, Clone, Serialize, PartialEq)] 36 | #[serde(tag = "type")] 37 | pub enum ProcResult { 38 | Exit { 39 | status: i32, 40 | }, 41 | Signal { 42 | signal: Option, 43 | core_dumped: bool, 44 | }, 45 | Closed, 46 | } 47 | 48 | pub trait ProcCallback { 49 | fn rx(&self, fd: u32, data: &[u8]); 50 | } 51 | -------------------------------------------------------------------------------- /src-tauri/src/shell_manager/manager.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::sync::Arc; 3 | 4 | use crate::app_dirs::{GetSshDir, SetSshDir}; 5 | use crate::device_manager::Device; 6 | use crate::error::Error; 7 | use crate::shell_manager::{Shell, ShellInfo, ShellManager, ShellToken}; 8 | 9 | impl ShellManager { 10 | pub fn open(&self, device: Device, rows: u16, cols: u16, dumb: bool) -> Arc { 11 | let shell = Arc::new(Shell::new( 12 | device, 13 | self.get_ssh_dir().as_deref(), 14 | !dumb, 15 | rows, 16 | cols, 17 | self.shells.clone(), 18 | )); 19 | self.shells 20 | .lock() 21 | .unwrap() 22 | .insert(shell.token.clone(), shell.clone()); 23 | Shell::thread(shell.clone()); 24 | shell 25 | } 26 | 27 | pub fn find(&self, token: &ShellToken) -> Option> { 28 | self.shells.lock().unwrap().get(token).map(|a| a.clone()) 29 | } 30 | 31 | pub fn close(&self, token: &ShellToken) -> Result<(), Error> { 32 | let shell = self.shells.lock().unwrap().remove(&token).clone(); 33 | if let Some(shell) = shell { 34 | shell.close().unwrap_or(()); 35 | } 36 | Ok(()) 37 | } 38 | 39 | pub fn list(&self) -> Vec { 40 | let mut list: Vec = self 41 | .shells 42 | .lock() 43 | .unwrap() 44 | .iter() 45 | .map(|(_, shell)| shell.info()) 46 | .collect(); 47 | list.sort_by_key(|v| v.created_at); 48 | list 49 | } 50 | } 51 | 52 | impl GetSshDir for ShellManager { 53 | fn get_ssh_dir(&self) -> Option { 54 | self.ssh_dir.lock().unwrap().clone() 55 | } 56 | } 57 | 58 | impl SetSshDir for ShellManager { 59 | fn set_ssh_dir(&self, dir: PathBuf) { 60 | *self.ssh_dir.lock().unwrap() = Some(dir); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src-tauri/src/shell_manager/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::sync::mpsc::Sender; 3 | use std::sync::{Arc, Mutex}; 4 | use std::time::Instant; 5 | 6 | use serde::Serialize; 7 | use uuid::Uuid; 8 | use vt100::Parser; 9 | 10 | use crate::device_manager::Device; 11 | use crate::error::Error; 12 | use crate::shell_manager::shell::ShellsMap; 13 | 14 | pub(crate) mod manager; 15 | pub(crate) mod shell; 16 | pub(crate) mod token; 17 | 18 | #[derive(Default)] 19 | pub struct ShellManager { 20 | pub(crate) shells: Arc>, 21 | ssh_dir: Mutex>, 22 | } 23 | 24 | pub struct Shell { 25 | pub token: ShellToken, 26 | created_at: Instant, 27 | device: Device, 28 | ssh_dir: Option, 29 | pub(crate) has_pty: Mutex>, 30 | pub(crate) closed: Mutex>, 31 | pub(crate) sender: Mutex>>, 32 | pub(crate) callback: Mutex>>, 33 | pub(crate) parser: Mutex, 34 | pub(crate) shells: Arc>, 35 | } 36 | 37 | pub trait ShellCallback { 38 | fn info(&self, info: ShellInfo); 39 | fn rx(&self, fd: u32, data: &[u8]); 40 | fn closed(&self, removed: bool); 41 | } 42 | 43 | #[derive(PartialEq, Eq, Hash, Clone, Debug)] 44 | pub struct ShellToken(Uuid); 45 | 46 | #[derive(Clone, Serialize, Debug)] 47 | pub struct ShellInfo { 48 | pub token: ShellToken, 49 | pub title: String, 50 | pub state: ShellState, 51 | #[serde(rename = "hasPty", skip_serializing_if = "Option::is_none")] 52 | pub has_pty: Option, 53 | #[serde(skip_serializing)] 54 | created_at: Instant, 55 | } 56 | 57 | #[derive(Hash, Clone, Debug, Serialize)] 58 | pub struct ShellData { 59 | pub token: ShellToken, 60 | pub fd: u32, 61 | pub data: Vec, 62 | } 63 | 64 | #[derive(Clone, Serialize, Debug)] 65 | pub struct ShellScreen { 66 | rows: Option>>, 67 | data: Option>, 68 | cursor: (u16, u16), 69 | } 70 | 71 | pub(crate) enum ShellMessage { 72 | Data(Vec), 73 | Resize { rows: u16, cols: u16 }, 74 | Close, 75 | } 76 | 77 | #[derive(Clone, Serialize, Debug)] 78 | #[serde(tag = "which")] 79 | pub enum ShellState { 80 | Connecting, 81 | Connected, 82 | Exited { 83 | #[serde(rename = "returnCode")] 84 | return_code: i32, 85 | }, 86 | Error { 87 | error: Error, 88 | }, 89 | } 90 | -------------------------------------------------------------------------------- /src-tauri/src/shell_manager/token.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::fmt::{Display, Formatter}; 3 | use std::str::FromStr; 4 | 5 | use serde::de::Visitor; 6 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 7 | use uuid::Uuid; 8 | 9 | use crate::shell_manager::ShellToken; 10 | 11 | impl ShellToken { 12 | pub(crate) fn new() -> Self { 13 | ShellToken(Uuid::new_v4()) 14 | } 15 | } 16 | impl Serialize for ShellToken { 17 | fn serialize(&self, serializer: S) -> Result 18 | where 19 | S: Serializer, 20 | { 21 | serializer.serialize_str(&format!("{}", self.0)) 22 | } 23 | } 24 | 25 | impl<'de> Deserialize<'de> for ShellToken { 26 | fn deserialize(deserializer: D) -> Result 27 | where 28 | D: Deserializer<'de>, 29 | { 30 | deserializer.deserialize_string(ShellTokenVisitor) 31 | } 32 | } 33 | 34 | struct ShellTokenVisitor; 35 | 36 | impl<'de> Visitor<'de> for ShellTokenVisitor { 37 | type Value = ShellToken; 38 | 39 | fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 40 | formatter.write_str("string") 41 | } 42 | 43 | // parse the version from the string 44 | fn visit_str(self, value: &str) -> Result 45 | where 46 | E: std::error::Error, 47 | { 48 | Ok(ShellToken(Uuid::from_str(value).unwrap())) 49 | } 50 | } 51 | 52 | impl Display for ShellToken { 53 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 54 | f.write_str(&self.0.to_string()) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src-tauri/src/spawn_manager/manager.rs: -------------------------------------------------------------------------------- 1 | use crate::session_manager::Proc; 2 | use crate::spawn_manager::SpawnManager; 3 | use std::sync::Arc; 4 | 5 | impl SpawnManager { 6 | pub fn add_proc(&self, proc: Arc) { 7 | self.items 8 | .lock() 9 | .expect("Failed to lock SpawnManager::items") 10 | .push(Arc::downgrade(&proc)) 11 | } 12 | 13 | pub fn clear(&self) { 14 | let mut guard = self 15 | .items 16 | .lock() 17 | .expect("Failed to lock SpawnManager::items"); 18 | let old_items = std::mem::replace(&mut *guard, Vec::new()); 19 | drop(guard); 20 | for x in old_items { 21 | if let Some(proc) = x.upgrade() { 22 | log::debug!("Terminating {proc:?}"); 23 | proc.interrupt(); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src-tauri/src/spawn_manager/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::session_manager::Proc; 2 | use std::sync::{Mutex, Weak}; 3 | 4 | mod manager; 5 | 6 | #[derive(Default)] 7 | pub(crate) struct SpawnManager { 8 | items: Mutex>>, 9 | } 10 | -------------------------------------------------------------------------------- /src-tauri/src/tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | mod test_server; 2 | 3 | pub use test_server::SshContainer; 4 | -------------------------------------------------------------------------------- /src-tauri/src/tests/common/test_server/entrypoint.d/setpasswd.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | echo "root:alpine" | chpasswd 6 | -------------------------------------------------------------------------------- /src-tauri/src/tests/common/test_server/keys/id_root: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS 3 | 1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTqu3dj7L8aVOTlvR/+wFfzn/t7VA4H 4 | sA9wPRBtLxeOePcxYtXb+a/htuy0I7VKQxyNk6u3yz3AP5rImvc0po+4AAAAsBDlh8UQ5Y 5 | fFAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOq7d2PsvxpU5OW9 6 | H/7AV/Of+3tUDgewD3A9EG0vF4549zFi1dv5r+G27LQjtUpDHI2Tq7fLPcA/msia9zSmj7 7 | gAAAAgDz8yxLi2BFo+MQ9b4s6a8YZG2pXElVd/oP3kk3em8U4AAAAXbWFyaW90YWt1QE1B 8 | UklPVEFLVS1OVUMB 9 | -----END OPENSSH PRIVATE KEY----- 10 | -------------------------------------------------------------------------------- /src-tauri/src/tests/common/test_server/keys/id_root.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOq7d2PsvxpU5OW9H/7AV/Of+3tUDgewD3A9EG0vF4549zFi1dv5r+G27LQjtUpDHI2Tq7fLPcA/msia9zSmj7g= mariotaku@MARIOTAKU-NUC 2 | -------------------------------------------------------------------------------- /src-tauri/src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod common; 2 | -------------------------------------------------------------------------------- /src-tauri/tauri.linux.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "productName": "webos-dev-manager" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/add-device/add-device.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/add-device/add-device.component.scss -------------------------------------------------------------------------------- /src/app/add-device/add-device.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule, NgOptimizedImage} from '@angular/common'; 3 | import {FormsModule, ReactiveFormsModule} from "@angular/forms"; 4 | import {WizardComponent, WizardFooterTemplateDirective} from './wizard/wizard.component'; 5 | import {SharedModule} from "../shared/shared.module"; 6 | import {NgbAccordionModule, NgbCollapse, NgbNavModule, NgbTooltipModule} from "@ng-bootstrap/ng-bootstrap"; 7 | import {ModeSelectComponent} from './wizard/mode-select/mode-select.component'; 8 | import {DevmodeSetupComponent} from './wizard/devmode-setup/devmode-setup.component'; 9 | import {StepHeaderComponent} from './wizard/devmode-setup/step-header/step-header.component'; 10 | import {AddDeviceComponent} from "./wizard/add-device/add-device.component"; 11 | import {KeyPassphrasePromptComponent} from './device-editor/key-passphrase-prompt/key-passphrase-prompt.component'; 12 | import { 13 | DevmodePassphraseHintComponent 14 | } from './device-editor/devmode-passphrase-hint/devmode-passphrase-hint.component'; 15 | import {SshPrivkeyHintComponent} from './device-editor/ssh-privkey-hint/ssh-privkey-hint.component'; 16 | import {SshPasswordHintComponent} from './device-editor/ssh-password-hint/ssh-password-hint.component'; 17 | import {RetryFailedComponent} from './retry-failed/retry-failed.component'; 18 | import {SshAuthValueDirective} from "./device-editor/ssh-auth-value.directive"; 19 | import {DeviceEditorComponent} from "./device-editor/device-editor.component"; 20 | 21 | 22 | @NgModule({ 23 | declarations: [ 24 | WizardComponent, 25 | ModeSelectComponent, 26 | AddDeviceComponent, 27 | DevmodeSetupComponent, 28 | StepHeaderComponent, 29 | KeyPassphrasePromptComponent, 30 | DevmodePassphraseHintComponent, 31 | SshPrivkeyHintComponent, 32 | SshPasswordHintComponent, 33 | RetryFailedComponent, 34 | ], 35 | imports: [ 36 | CommonModule, 37 | ReactiveFormsModule, 38 | SharedModule, 39 | NgbNavModule, 40 | NgbAccordionModule, 41 | NgbTooltipModule, 42 | FormsModule, 43 | NgbCollapse, 44 | NgOptimizedImage, 45 | SshAuthValueDirective, 46 | DeviceEditorComponent, 47 | WizardFooterTemplateDirective, 48 | ] 49 | }) 50 | export class AddDeviceModule { 51 | } 52 | -------------------------------------------------------------------------------- /src/app/add-device/conn-hint/conn-hint.component.html: -------------------------------------------------------------------------------- 1 |

2 | Please check the address and port you have entered, make sure "Developer Mode" is installed and turned 3 | on. And you have good network connection between your computer and your TV.
4 | Check out 5 | Installing Developer Mode App for more info. 6 |

7 | -------------------------------------------------------------------------------- /src/app/add-device/conn-hint/conn-hint.component.scss: -------------------------------------------------------------------------------- 1 | img { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/add-device/conn-hint/conn-hint.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-conn-hint', 5 | templateUrl: './conn-hint.component.html', 6 | styleUrls: ['./conn-hint.component.scss'] 7 | }) 8 | export class ConnHintComponent { 9 | 10 | constructor() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/app/add-device/device-editor/device-editor.component.scss: -------------------------------------------------------------------------------- 1 | .address-input { 2 | .host { 3 | } 4 | 5 | .port { 6 | max-width: 6em; 7 | } 8 | } 9 | 10 | .auth-input { 11 | select { 12 | max-width: 10em; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/add-device/device-editor/devmode-passphrase-hint/devmode-passphrase-hint.component.html: -------------------------------------------------------------------------------- 1 |

Please use the passphrase displayed in Dev Mode app as screenshot below.

2 | Developer Mode passphrase 3 | -------------------------------------------------------------------------------- /src/app/add-device/device-editor/devmode-passphrase-hint/devmode-passphrase-hint.component.scss: -------------------------------------------------------------------------------- 1 | img { 2 | max-width: 100%; 3 | height: auto; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/add-device/device-editor/devmode-passphrase-hint/devmode-passphrase-hint.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-devmode-passphrase-hint', 5 | templateUrl: './devmode-passphrase-hint.component.html', 6 | styleUrls: ['./devmode-passphrase-hint.component.scss'] 7 | }) 8 | export class DevmodePassphraseHintComponent { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/app/add-device/device-editor/key-passphrase-prompt/key-passphrase-prompt.component.html: -------------------------------------------------------------------------------- 1 | 10 | 16 | -------------------------------------------------------------------------------- /src/app/add-device/device-editor/key-passphrase-prompt/key-passphrase-prompt.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/add-device/device-editor/key-passphrase-prompt/key-passphrase-prompt.component.scss -------------------------------------------------------------------------------- /src/app/add-device/device-editor/key-passphrase-prompt/key-passphrase-prompt.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject, Injector} from '@angular/core'; 2 | import {NgbActiveModal, NgbModal} from "@ng-bootstrap/ng-bootstrap"; 3 | import {FormControl, ValidationErrors} from "@angular/forms"; 4 | import {DeviceManagerService} from "../../../core/services"; 5 | import {Observable} from "rxjs"; 6 | import {BackendError} from "../../../core/services/backend-client"; 7 | import {fromPromise} from "rxjs/internal/observable/innerFrom"; 8 | 9 | @Component({ 10 | selector: 'app-key-passphrase-prompt', 11 | templateUrl: './key-passphrase-prompt.component.html', 12 | styleUrls: ['./key-passphrase-prompt.component.scss'] 13 | }) 14 | export class KeyPassphrasePromptComponent { 15 | formControl: FormControl; 16 | 17 | constructor( 18 | public modal: NgbActiveModal, 19 | private deviceManager: DeviceManagerService, 20 | @Inject('keyPath') private keyPath: string 21 | ) { 22 | this.formControl = new FormControl('', { 23 | nonNullable: true, 24 | asyncValidators: (control) => this.verifySshKey(control.value) 25 | }); 26 | } 27 | 28 | private verifySshKey(passphrase: string): Observable { 29 | return fromPromise(this.deviceManager.verifyLocalPrivateKey(this.keyPath, passphrase).then(() => null).catch(e => { 30 | if (BackendError.isCompatibleBody(e)) { 31 | return {[e.reason]: true}; 32 | } 33 | throw e; 34 | })); 35 | } 36 | 37 | static async prompt(modals: NgbModal, keyPath: string): Promise { 38 | const ref = modals.open(KeyPassphrasePromptComponent, { 39 | size: 'sm', 40 | centered: true, 41 | injector: Injector.create({ 42 | providers: [ 43 | {provide: 'keyPath', useValue: keyPath} 44 | ] 45 | }) 46 | }); 47 | return ref.result.catch(() => undefined); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/add-device/device-editor/ssh-auth-value.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import {SshAuthValueDirective} from './ssh-auth-value.directive'; 2 | import {Component} from "@angular/core"; 3 | import {NewDeviceAuthentication} from "../../types"; 4 | import {TestBed} from "@angular/core/testing"; 5 | 6 | describe('SshAuthValueDirective', () => { 7 | it('should create an instance', () => { 8 | const component = TestBed.createComponent(TestSshAuthValueDirectiveHostComponent); 9 | }); 10 | }); 11 | 12 | @Component({ 13 | selector: 'app-test-ssh-auth-value-directive-host', 14 | template: `` 17 | }) 18 | class TestSshAuthValueDirectiveHostComponent { 19 | protected readonly NewDeviceAuthentication = NewDeviceAuthentication; 20 | } 21 | -------------------------------------------------------------------------------- /src/app/add-device/device-editor/ssh-password-hint/ssh-password-hint.component.html: -------------------------------------------------------------------------------- 1 |

2 | If you have rooted your TV and installed Homebrew Channel, the default root password for SSH is alpine. 3 |
4 | Please note that using default password is a security risk. Using private key based authentication is recommended. 5 |

6 | -------------------------------------------------------------------------------- /src/app/add-device/device-editor/ssh-password-hint/ssh-password-hint.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/add-device/device-editor/ssh-password-hint/ssh-password-hint.component.scss -------------------------------------------------------------------------------- /src/app/add-device/device-editor/ssh-password-hint/ssh-password-hint.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-ssh-password-hint', 5 | templateUrl: './ssh-password-hint.component.html', 6 | styleUrls: ['./ssh-password-hint.component.scss'] 7 | }) 8 | export class SshPasswordHintComponent { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/app/add-device/device-editor/ssh-privkey-hint/ssh-privkey-hint.component.html: -------------------------------------------------------------------------------- 1 |

Private keys are usually located in following location:

2 |
    3 |
  • On Windows: C:\Users\USERNAME\.ssh.
  • 4 |
  • On macOS and Linux: ~/.ssh.
  • 5 |
6 |

7 | Usually, you'll find files named id_rsa, id_ecdsa, id_ed25519. That's your 8 | private key file. 9 |
10 | If you don't have such key, you will need to create one. Please search usage of ssh-keygen for more 11 | details. 12 |

13 | -------------------------------------------------------------------------------- /src/app/add-device/device-editor/ssh-privkey-hint/ssh-privkey-hint.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/add-device/device-editor/ssh-privkey-hint/ssh-privkey-hint.component.scss -------------------------------------------------------------------------------- /src/app/add-device/device-editor/ssh-privkey-hint/ssh-privkey-hint.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-ssh-privkey-hint', 5 | templateUrl: './ssh-privkey-hint.component.html', 6 | styleUrls: ['./ssh-privkey-hint.component.scss'] 7 | }) 8 | export class SshPrivkeyHintComponent { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/app/add-device/keyserver-hint/keyserver-hint.component.html: -------------------------------------------------------------------------------- 1 |

Please make sure "Key Server" in Developer Mode App is turned on during device setup.

2 | Turn "Key Server" on in Developer Mode App 4 |

If you have already turned on "Key Server" but still cannot connect to the TV, you can try revoke all the 5 | user agreements and re-enable developer mode.

6 |

See also

7 | 22 | -------------------------------------------------------------------------------- /src/app/add-device/keyserver-hint/keyserver-hint.component.scss: -------------------------------------------------------------------------------- 1 | img { 2 | max-width: 100%; 3 | height: auto; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/add-device/keyserver-hint/keyserver-hint.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-keyserver-hint', 5 | templateUrl: './keyserver-hint.component.html', 6 | styleUrls: ['./keyserver-hint.component.scss'] 7 | }) 8 | export class KeyserverHintComponent { 9 | 10 | constructor() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/app/add-device/retry-failed/retry-failed.component.html: -------------------------------------------------------------------------------- 1 |

2 | Failed after too many attempts. 3 | If you think this is a bug, please report it here. 5 |

6 | -------------------------------------------------------------------------------- /src/app/add-device/retry-failed/retry-failed.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/add-device/retry-failed/retry-failed.component.scss -------------------------------------------------------------------------------- /src/app/add-device/retry-failed/retry-failed.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-retry-failed', 5 | templateUrl: './retry-failed.component.html', 6 | styleUrls: ['./retry-failed.component.scss'] 7 | }) 8 | export class RetryFailedComponent { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/app/add-device/wizard/add-device/add-device.component.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/app/add-device/wizard/add-device/add-device.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/add-device/wizard/add-device/add-device.component.scss -------------------------------------------------------------------------------- /src/app/add-device/wizard/add-device/add-device.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core'; 2 | import {DeviceConnectionMode} from "../mode-select/mode-select.component"; 3 | import {Device, NewDeviceAuthentication} from "../../../types"; 4 | import {DeviceEditorComponent, SetupAuthInfoUnion} from "../../device-editor/device-editor.component"; 5 | import {ProgressDialogComponent} from "../../../shared/components/progress-dialog/progress-dialog.component"; 6 | import {NgbModal} from "@ng-bootstrap/ng-bootstrap"; 7 | import {DeviceManagerService} from "../../../core/services"; 8 | 9 | @Component({ 10 | selector: 'app-wizard-add-device', 11 | templateUrl: './add-device.component.html', 12 | styleUrls: ['./add-device.component.scss'] 13 | }) 14 | export class AddDeviceComponent implements OnInit { 15 | @Input() 16 | mode!: DeviceConnectionMode; 17 | 18 | @ViewChild('deviceEditor') 19 | deviceEditor!: DeviceEditorComponent; 20 | 21 | username?: string; 22 | port?: number; 23 | auth?: SetupAuthInfoUnion; 24 | 25 | @Output() 26 | deviceAdded: EventEmitter = new EventEmitter(); 27 | 28 | constructor(private modals: NgbModal, private deviceManager: DeviceManagerService) { 29 | } 30 | 31 | ngOnInit(): void { 32 | switch (this.mode) { 33 | case DeviceConnectionMode.DevMode: { 34 | this.username = 'prisoner'; 35 | this.port = 9922; 36 | this.auth = {type: NewDeviceAuthentication.DevKey, value: ''}; 37 | break; 38 | } 39 | case DeviceConnectionMode.Rooted: { 40 | this.username = 'root'; 41 | this.port = 22; 42 | break; 43 | } 44 | } 45 | } 46 | 47 | get valid(): boolean { 48 | return this.deviceEditor.valid; 49 | } 50 | 51 | async submit(): Promise { 52 | const progress = ProgressDialogComponent.open(this.modals); 53 | try { 54 | const newDevice = await this.deviceEditor.submit(); 55 | this.deviceAdded.emit(await this.deviceManager.addDevice(newDevice)); 56 | } finally { 57 | progress.close(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/app/add-device/wizard/devmode-setup/devmode-setup.component.html: -------------------------------------------------------------------------------- 1 |

Do following steps to get ready for setting up connection to your TV.

2 |
3 | 5 | 6 |
7 |

8 | You'll need a developer account to use Developer Mode. 9 | Please read 11 | Preparing LG Account for creating one. 12 |

13 |
14 |
15 |
16 | 17 | 18 |
19 |
    20 |
  1. Open LG Content Store
  2. 21 |
  3. Search for "Developer Mode"
  4. 22 |
  5. Select "Install"
  6. 23 |
24 |
25 |
26 |
27 | 28 | 29 |
30 |
    31 |
  1. Launch Developer Mode app
  2. 32 |
  3. Login to Developer Mode app with developer account
  4. 33 |
  5. Enable Developer Mode and wait for TV to restart
  6. 34 |
35 |
36 |
37 |
38 | 39 | 40 |
41 |
    42 |
  1. Launch Developer Mode app again
  2. 43 |
  3. Ensure "Dev Mode Status" is ON
  4. 44 |
  5. Enable Key Server 45 | 46 |
  6. 47 |
48 |
49 |
50 | -------------------------------------------------------------------------------- /src/app/add-device/wizard/devmode-setup/devmode-setup.component.scss: -------------------------------------------------------------------------------- 1 | .devmode-setup-steps { 2 | .accordion-button { 3 | background: none !important; 4 | 5 | &::after { 6 | background-image: none; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/add-device/wizard/devmode-setup/devmode-setup.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, ViewEncapsulation} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-wizard-devmode-setup', 5 | templateUrl: './devmode-setup.component.html', 6 | styleUrls: ['./devmode-setup.component.scss'], 7 | encapsulation: ViewEncapsulation.None 8 | }) 9 | export class DevmodeSetupComponent { 10 | 11 | prepareAccountDone: boolean = false; 12 | installAppDone: boolean = false; 13 | enableDevModeDone: boolean = false; 14 | prepareSetupDone: boolean = false; 15 | 16 | get allDone(): boolean { 17 | return this.prepareAccountDone && this.installAppDone && this.enableDevModeDone && this.prepareSetupDone; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/add-device/wizard/devmode-setup/step-header/step-header.component.html: -------------------------------------------------------------------------------- 1 |
2 |
{{title}}
3 |
4 | 6 | 7 |
8 |
9 | -------------------------------------------------------------------------------- /src/app/add-device/wizard/devmode-setup/step-header/step-header.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/add-device/wizard/devmode-setup/step-header/step-header.component.scss -------------------------------------------------------------------------------- /src/app/add-device/wizard/devmode-setup/step-header/step-header.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Input, Output} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-devmode-setup-step-header', 5 | templateUrl: './step-header.component.html', 6 | styleUrls: ['./step-header.component.scss'] 7 | }) 8 | export class StepHeaderComponent { 9 | private checked: boolean = false; 10 | 11 | @Input() 12 | id: string = ''; 13 | 14 | @Input() 15 | title: string = ''; 16 | 17 | @Output() 18 | doneChange: EventEmitter = new EventEmitter(); 19 | 20 | get done(): boolean { 21 | return this.checked; 22 | } 23 | 24 | @Input() 25 | set done(done: boolean) { 26 | this.checked = done; 27 | this.doneChange.emit(done); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/add-device/wizard/mode-select/mode-select.component.html: -------------------------------------------------------------------------------- 1 |

If your TV is not rooted, or not sure what to choose, you can leave Developer Mode unchanged.

2 | 3 |
4 | 6 | 9 |
10 |
11 | 13 | 16 |
17 |
18 | 20 | 23 |
24 | -------------------------------------------------------------------------------- /src/app/add-device/wizard/mode-select/mode-select.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/add-device/wizard/mode-select/mode-select.component.scss -------------------------------------------------------------------------------- /src/app/add-device/wizard/mode-select/mode-select.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Input, Output} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-wizard-mode-select', 5 | templateUrl: './mode-select.component.html', 6 | styleUrls: ['./mode-select.component.scss'] 7 | }) 8 | export class ModeSelectComponent { 9 | private modeValue: DeviceConnectionMode = DeviceConnectionMode.DevMode; 10 | 11 | @Output() 12 | public modeChange: EventEmitter = new EventEmitter(); 13 | 14 | @Output() 15 | public proceed: EventEmitter = new EventEmitter(); 16 | 17 | get mode(): DeviceConnectionMode { 18 | return this.modeValue; 19 | } 20 | 21 | @Input() 22 | set mode(mode: DeviceConnectionMode) { 23 | this.modeValue = mode; 24 | this.modeChange.emit(mode); 25 | } 26 | } 27 | 28 | export enum DeviceConnectionMode { 29 | DevMode = 'devMode', 30 | Rooted = 'rooted', 31 | Advanced = 'advanced', 32 | } 33 | -------------------------------------------------------------------------------- /src/app/add-device/wizard/wizard.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/add-device/wizard/wizard.component.scss -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {HomeComponent} from './home/home.component'; 4 | import {NavMoreComponent} from "./home/nav-more/nav-more.component"; 5 | 6 | 7 | const routes: Routes = [ 8 | { 9 | path: '', 10 | redirectTo: 'home', 11 | pathMatch: 'full' 12 | }, 13 | { 14 | path: 'home', component: HomeComponent, 15 | children: [ 16 | {path: 'apps', loadChildren: () => import('./apps').then(m => m.AppsModule)}, 17 | {path: 'files', loadChildren: () => import('./files').then(m => m.FilesModule)}, 18 | {path: 'terminal', loadChildren: () => import('./terminal').then(m => m.TerminalModule)}, 19 | {path: 'debug', loadChildren: () => import('./debug').then(m => m.DebugModule)}, 20 | {path: 'info', loadChildren: () => import('./info').then(m => m.InfoModule)}, 21 | {path: 'devices', loadComponent: () => import('./devices/devices.component').then(m => m.DevicesComponent)}, 22 | {path: 'nav-more', component: NavMoreComponent}, 23 | {path: '', redirectTo: 'apps', pathMatch: 'full'}, 24 | ] 25 | }, 26 | { 27 | path: '**', 28 | redirectTo: 'home' 29 | } 30 | ]; 31 | 32 | @NgModule({ 33 | imports: [ 34 | RouterModule.forRoot(routes), 35 | ], 36 | exports: [RouterModule] 37 | }) 38 | export class AppRoutingModule { 39 | } 40 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | width: 100%; 3 | height: 100%; 4 | display: block; 5 | overflow: hidden; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {DeviceManagerService, Release, UpdateService} from './core/services'; 3 | import PackageInfo from '../../package.json'; 4 | import {SemVer} from 'semver'; 5 | import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; 6 | import {MessageDialogComponent} from './shared/components/message-dialog/message-dialog.component'; 7 | import {UpdateDetailsComponent} from './update-details/update-details.component'; 8 | import {open} from "@tauri-apps/plugin-shell"; 9 | import {noop} from "rxjs"; 10 | 11 | @Component({ 12 | selector: 'app-root', 13 | templateUrl: './app.component.html', 14 | styleUrls: ['./app.component.scss'] 15 | }) 16 | export class AppComponent { 17 | 18 | constructor( 19 | private update: UpdateService, 20 | private modalService: NgbModal, 21 | private deviceManager: DeviceManagerService 22 | ) { 23 | update.getRecentRelease().then(async info => { 24 | let curVer = new SemVer(PackageInfo.version, true); 25 | const until = update.ignoreUntil; 26 | if (until && curVer.compare(until) < 0) { 27 | curVer = new SemVer(until, true); 28 | } 29 | const remoteVer = new SemVer(info.tag_name, true); 30 | if (remoteVer.compare(curVer) > 0) { 31 | await this.notifyUpdate(info, remoteVer); 32 | } 33 | }).catch(noop); 34 | deviceManager.load(); 35 | } 36 | 37 | private async notifyUpdate(info: Release, version: SemVer): Promise { 38 | return MessageDialogComponent.open(this.modalService, { 39 | title: `Update ${version.version} is available`, 40 | message: UpdateDetailsComponent, 41 | negative: 'Next time', 42 | positive: 'More info', 43 | alternative: 'Ignore this version', 44 | messageExtras: {release: info} 45 | }).result.then((result: boolean | null) => { 46 | switch (result) { 47 | case true: { 48 | open(info.html_url); 49 | break; 50 | } 51 | case false: { 52 | break; 53 | } 54 | case null: { 55 | this.update.ignoreUntil = version; 56 | break; 57 | } 58 | } 59 | }).catch(noop); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/app/apps/apps-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {AppsComponent} from "./apps.component"; 4 | 5 | const routes: Routes = [ 6 | {path: '', component: AppsComponent} 7 | ]; 8 | 9 | @NgModule({ 10 | imports: [RouterModule.forChild(routes)], 11 | exports: [RouterModule] 12 | }) 13 | export class AppsRoutingModule { 14 | } 15 | -------------------------------------------------------------------------------- /src/app/apps/apps.component.html: -------------------------------------------------------------------------------- 1 |
2 | 29 |
30 |
31 | 32 |
33 |
34 | -------------------------------------------------------------------------------- /src/app/apps/apps.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/apps/apps.component.scss -------------------------------------------------------------------------------- /src/app/apps/apps.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule, NgOptimizedImage} from '@angular/common'; 3 | 4 | import {AppsRoutingModule} from './apps-routing.module'; 5 | import {AppsComponent} from "./apps.component"; 6 | import {ChannelComponent} from "./channel/channel.component"; 7 | import {InstalledComponent} from "./installed/installed.component"; 8 | import {NgbDropdownModule, NgbNavModule, NgbPaginationModule, NgbProgressbar} from "@ng-bootstrap/ng-bootstrap"; 9 | import {SharedModule} from "../shared/shared.module"; 10 | import {HbchannelRemoveComponent} from './hbchannel-remove/hbchannel-remove.component'; 11 | import {FormsModule} from "@angular/forms"; 12 | 13 | @NgModule({ 14 | declarations: [ 15 | AppsComponent, 16 | InstalledComponent, 17 | ChannelComponent, 18 | HbchannelRemoveComponent, 19 | ], 20 | imports: [ 21 | CommonModule, 22 | NgbNavModule, 23 | NgbPaginationModule, 24 | AppsRoutingModule, 25 | SharedModule, 26 | NgbDropdownModule, 27 | NgbProgressbar, 28 | NgOptimizedImage, 29 | FormsModule, 30 | ] 31 | }) 32 | export class AppsModule { 33 | } 34 | -------------------------------------------------------------------------------- /src/app/apps/channel/channel.component.html: -------------------------------------------------------------------------------- 1 | @let repoPage = repoPage$ | async; 2 | @if (repoPage) { 3 |
    4 |
  • 6 | @let manifest = item.manifest; 7 | @if (manifest) { 8 |
    9 | 10 |
    11 |
    {{ item.title }}
    12 |
    {{ manifest.appDescription }}
    13 |
    14 |
    15 | 16 | } 17 |
  • 18 |
19 |
20 | 22 | 23 |
24 | } @else { 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/app/apps/channel/channel.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/apps/channel/channel.component.scss -------------------------------------------------------------------------------- /src/app/apps/channel/channel.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Host, Input, OnInit} from '@angular/core'; 2 | import {Observable} from 'rxjs'; 3 | import {AppsRepoService, RepositoryPage} from '../../core/services'; 4 | import {AppsComponent} from '../apps.component'; 5 | import {RawPackageInfo} from "../../types"; 6 | 7 | @Component({ 8 | selector: 'app-channel', 9 | templateUrl: './channel.component.html', 10 | styleUrls: ['./channel.component.scss'] 11 | }) 12 | export class ChannelComponent implements OnInit { 13 | 14 | page = 1; 15 | repoPage$?: Observable; 16 | 17 | @Input() 18 | installed?: Record; 19 | 20 | constructor( 21 | @Host() public parent: AppsComponent, 22 | private appsRepo: AppsRepoService) { 23 | } 24 | 25 | ngOnInit(): void { 26 | this.loadPage(1); 27 | } 28 | 29 | loadPage(page: number): void { 30 | this.repoPage$ = this.appsRepo.allApps$(page); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/apps/details/details.component.scss: -------------------------------------------------------------------------------- 1 | .full-description { 2 | img { 3 | max-width: 100%; 4 | } 5 | } 6 | 7 | .app-details-icon { 8 | max-width: 25vw; 9 | max-height: 25vw; 10 | height: auto; 11 | object-fit: contain; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/apps/hbchannel-remove/hbchannel-remove.component.html: -------------------------------------------------------------------------------- 1 |

Danger!

2 |

You're going to remove Homebrew Channel. For rooted TV, you will lose root 3 | immediately!

4 | -------------------------------------------------------------------------------- /src/app/apps/hbchannel-remove/hbchannel-remove.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/apps/hbchannel-remove/hbchannel-remove.component.scss -------------------------------------------------------------------------------- /src/app/apps/hbchannel-remove/hbchannel-remove.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-hbchannel-remove', 5 | templateUrl: './hbchannel-remove.component.html', 6 | styleUrls: ['./hbchannel-remove.component.scss'] 7 | }) 8 | export class HbchannelRemoveComponent { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/app/apps/index.ts: -------------------------------------------------------------------------------- 1 | export {AppsModule} from './apps.module'; 2 | -------------------------------------------------------------------------------- /src/app/apps/installed/installed.component.html: -------------------------------------------------------------------------------- 1 | @let installed = installed$ | async; 2 | @if (installedError) { 3 | 4 | 5 | } @else if (installed) { 6 |
    7 | @for (pkg of installed; track pkg.id) { 8 |
  • 10 |
    11 | 12 | 13 |
    14 |
    {{ pkg.title }}
    15 |
    16 | v{{ pkg.version }} 17 | @let rpkg = repoPackages && repoPackages[pkg.id]; 18 | @if (rpkg && rpkg.manifest?.hasUpdate(pkg.version)) { 19 |  › v{{ rpkg.manifest?.version }} 20 | } 21 |
    22 |
    23 |
    24 | @if (repoPackages?.[pkg.id]?.manifest?.hasUpdate(pkg.version)) { 25 | 29 | } 30 |
  • 31 | } 32 |
33 | } @else { 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/app/apps/installed/installed.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/apps/installed/installed.component.scss -------------------------------------------------------------------------------- /src/app/apps/installed/installed.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Host, Input, OnDestroy} from '@angular/core'; 2 | import {AppsComponent} from '../apps.component'; 3 | import {Device, PackageInfo} from "../../types"; 4 | import {Observable, Subscription} from "rxjs"; 5 | import {AppsRepoService, RepositoryItem} from "../../core/services"; 6 | 7 | @Component({ 8 | selector: 'app-installed', 9 | templateUrl: './installed.component.html', 10 | styleUrls: ['./installed.component.scss'] 11 | }) 12 | export class InstalledComponent implements OnDestroy { 13 | 14 | @Input() 15 | device: Device | null = null; 16 | 17 | installedError?: Error; 18 | 19 | repoPackages?: Record; 20 | 21 | private subscription?: Subscription; 22 | private installedField?: Observable; 23 | 24 | constructor(@Host() public parent: AppsComponent, private appsRepo: AppsRepoService) { 25 | } 26 | 27 | @Input() 28 | set installed$(value: Observable | undefined) { 29 | this.subscription?.unsubscribe(); 30 | this.subscription = value?.subscribe({ 31 | next: (pkgs) => { 32 | this.installedError = undefined; 33 | 34 | const strings: string[] = pkgs?.map((pkg) => pkg.id) ?? []; 35 | this.appsRepo.showApps(...strings).then(apps => this.repoPackages = apps); 36 | }, 37 | error: (error) => { 38 | console.log('installed apps', error); 39 | return this.installedError = error; 40 | } 41 | }); 42 | this.installedField = value; 43 | } 44 | 45 | get installed$(): Observable | undefined { 46 | return this.installedField; 47 | } 48 | 49 | ngOnDestroy(): void { 50 | this.subscription?.unsubscribe(); 51 | } 52 | 53 | loadPackages(): void { 54 | this.installedError = undefined; 55 | this.parent.loadPackages(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | 4 | @NgModule({ 5 | imports: [ 6 | CommonModule 7 | ] 8 | }) 9 | export class CoreModule { 10 | } 11 | -------------------------------------------------------------------------------- /src/app/core/event-channel.ts: -------------------------------------------------------------------------------- 1 | import {emit, Event, listen, once, UnlistenFn} from "@tauri-apps/api/event"; 2 | 3 | export abstract class EventChannel { 4 | private promise?: Promise; 5 | private isClosed: boolean = false; 6 | 7 | protected constructor(protected token: string) { 8 | this.promise = Promise.all([ 9 | listen(`${token}:rx`, (e: Event) => { 10 | console.debug('event-channel::rx', this.token, e.payload); 11 | if (this.isClosed) { 12 | return; 13 | } 14 | this.onReceive(e.payload); 15 | }), 16 | once(`${token}:closed`, (e: Event) => { 17 | console.debug('event-channel::closed', this.token, e.payload); 18 | if (this.isClosed) { 19 | return; 20 | } 21 | this.isClosed = true; 22 | this.onClose(e.payload); 23 | }), 24 | ]); 25 | } 26 | 27 | public get closed(): boolean { 28 | return this.isClosed; 29 | } 30 | 31 | public async unlisten(): Promise { 32 | if (!this.promise) { 33 | return; 34 | } 35 | console.log('EventChannel', 'unlisten all'); 36 | await this.promise?.then(list => list.forEach(f => f?.())); 37 | this.promise = undefined; 38 | } 39 | 40 | public async send

(payload?: P): Promise { 41 | console.debug('event-channel::tx', this.token, payload); 42 | return emit(`${this.token}:tx`, payload); 43 | } 44 | 45 | public async close

(payload?: P): Promise { 46 | console.debug('event-channel::close', this.token, payload); 47 | return emit(`${this.token}:close`, payload); 48 | } 49 | 50 | abstract onReceive(payload: RxPayload): void; 51 | 52 | abstract onClose(payload: ClosePayload): void; 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/app/core/services/dev-mode.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, NgZone} from '@angular/core'; 2 | import {BackendClient} from "./backend-client"; 3 | import {Device} from "../../types"; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class DevModeService extends BackendClient { 9 | 10 | constructor(zone: NgZone) { 11 | super(zone, "dev-mode"); 12 | } 13 | 14 | async status(device: Device): Promise { 15 | return this.invoke('status', {device}); 16 | } 17 | 18 | async token(device: Device): Promise { 19 | return this.invoke('token', {device}); 20 | } 21 | } 22 | 23 | export interface DevModeStatus { 24 | token?: string; 25 | remaining?: string; 26 | } 27 | -------------------------------------------------------------------------------- /src/app/core/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./app-manager.service"; 2 | export * from "./device-manager.service"; 3 | export * from "./apps-repo.service"; 4 | export * from "./dev-mode.service"; 5 | export * from './update.service'; 6 | -------------------------------------------------------------------------------- /src/app/core/services/local-file.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, NgZone} from "@angular/core"; 2 | import {BackendClient} from "./backend-client"; 3 | import {ProgressCallback, progressChannel} from "./progress-callback"; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class LocalFileService extends BackendClient { 9 | constructor(zone: NgZone) { 10 | super(zone, 'local-file'); 11 | } 12 | 13 | async checksum(path: string, algorithm: 'sha256'): Promise { 14 | return this.invoke('checksum', {path, algorithm}); 15 | } 16 | 17 | async remove(path: string, recursive: boolean = false): Promise { 18 | await this.invoke('remove', {path, recursive}); 19 | } 20 | 21 | async copy(source: string, target: string, progress?: ProgressCallback): Promise { 22 | const onProgress = progressChannel(progress); 23 | await this.invoke('copy', {source, target, onProgress}); 24 | } 25 | 26 | async tempPath(extension: string): Promise { 27 | return this.invoke('temp_path', {extension}); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/core/services/progress-callback.ts: -------------------------------------------------------------------------------- 1 | import {Channel} from "@tauri-apps/api/core"; 2 | 3 | export type ProgressCallback = (copied: number, total: number) => void; 4 | 5 | interface ProgressPayload { 6 | copied: number; 7 | total: number; 8 | } 9 | 10 | export function progressChannel(progress?: ProgressCallback) { 11 | const onProgress = new Channel(); 12 | onProgress.onmessage = (e: ProgressPayload) => { 13 | progress?.(e.copied, e.total); 14 | } 15 | return onProgress; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/core/services/remote-command.service.spec.ts: -------------------------------------------------------------------------------- 1 | import {RemoteCommandService} from "./remote-command.service"; 2 | import {TestBed} from "@angular/core/testing"; 3 | import {NewDeviceWithLocalPrivateKey} from "../../types"; 4 | 5 | describe('RemoteCommandService', () => { 6 | let service: RemoteCommandService; 7 | let device = { 8 | name: 'test', 9 | host: '192.168.89.33', 10 | port: 22, 11 | username: 'root', 12 | profile: 'ose', 13 | privateKey: { 14 | openSsh: 'id_rsa' 15 | } 16 | }; 17 | 18 | beforeEach(() => { 19 | service = TestBed.inject(RemoteCommandService); 20 | }); 21 | 22 | it('should be created', () => { 23 | expect(service).toBeTruthy(); 24 | }); 25 | 26 | it('should successfully perform uname command', async () => { 27 | let output = service.exec(device, 'uname -a', 'utf-8'); 28 | await expectAsync(output).toBeResolved(); 29 | }); 30 | 31 | it('should run false command with exception', async () => { 32 | let output = service.exec(device, 'false', 'utf-8'); 33 | await expectAsync(output).toBeRejected(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/app/core/services/update.service.ts: -------------------------------------------------------------------------------- 1 | import {fetch} from '@tauri-apps/plugin-http'; 2 | import {Injectable} from '@angular/core'; 3 | import {SemVer} from 'semver'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class UpdateService { 9 | 10 | async getRecentRelease(): Promise { 11 | return fetch('https://api.github.com/repos/webosbrew/dev-manager-desktop/releases/latest', { 12 | headers: {'accept': 'application/vnd.github.v3+json'}, 13 | 14 | }).then(async res => new ReleaseImpl(await res.json())); 15 | } 16 | 17 | get ignoreUntil(): SemVer | null { 18 | try { 19 | const value = localStorage.getItem('devManager:ignoreVersionUntil'); 20 | if (!value) return null; 21 | return new SemVer(value, true); 22 | } catch (e) { 23 | return null; 24 | } 25 | } 26 | 27 | set ignoreUntil(value: SemVer | null) { 28 | if (value?.version) { 29 | localStorage.setItem('devManager:ignoreVersionUntil', value?.version); 30 | } else { 31 | localStorage.removeItem('devManager:ignoreVersionUntil'); 32 | } 33 | } 34 | } 35 | 36 | export interface Release { 37 | readonly html_url: string; 38 | readonly tag_name: string; 39 | readonly body: string; 40 | } 41 | 42 | class ReleaseImpl implements Release { 43 | html_url: string = ''; 44 | tag_name: string = ''; 45 | body: string = ''; 46 | 47 | constructor(data: Partial) { 48 | Object.assign(this, data); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/app/debug/crashes/crashes.component.html: -------------------------------------------------------------------------------- 1 | @if (reportsError) { 2 |

3 | 4 |
5 | } @else { 6 |
7 |
8 |
9 |
No crash reports
10 |

If a native application crashes, the crash report will be available here.

11 |
12 |
13 |
    14 |
  1. 16 |
    17 |
    {{ report.title }}
    18 | {{ report.summary }} 19 |
    20 |
    21 | 25 |
    26 |
  2. 27 |
28 |
29 | } 30 | -------------------------------------------------------------------------------- /src/app/debug/crashes/crashes.component.scss: -------------------------------------------------------------------------------- 1 | pre { 2 | //user-select: all; 3 | cursor: text; 4 | overflow-x: visible; 5 | } 6 | 7 | .report-entry { 8 | * { 9 | max-lines: 1; 10 | text-overflow: ellipsis; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/debug/crashes/details/details.component.html: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /src/app/debug/crashes/details/details.component.scss: -------------------------------------------------------------------------------- 1 | .modal-dialog-scrollable .modal-body { 2 | overflow-x: auto; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/debug/crashes/details/details.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {CrashReport} from "../../../core/services"; 3 | import {firstValueFrom} from "rxjs"; 4 | import {save as showSaveDialog} from "@tauri-apps/plugin-dialog"; 5 | import {ProgressDialogComponent} from "../../../shared/components/progress-dialog/progress-dialog.component"; 6 | import {writeTextFile} from "@tauri-apps/plugin-fs"; 7 | import {NgbActiveModal, NgbModal} from "@ng-bootstrap/ng-bootstrap"; 8 | 9 | @Component({ 10 | selector: 'app-crash-details', 11 | templateUrl: './details.component.html', 12 | styleUrls: ['./details.component.scss'] 13 | }) 14 | export class DetailsComponent { 15 | 16 | constructor(public report: CrashReport, public modal: NgbActiveModal, private modals: NgbModal) { 17 | } 18 | 19 | async copyReport(report: CrashReport): Promise { 20 | await navigator.clipboard.writeText(await firstValueFrom(report.content)); 21 | } 22 | 23 | async saveReport(report: CrashReport): Promise { 24 | let target: string | null; 25 | try { 26 | target = await showSaveDialog({ 27 | defaultPath: `${report.saveName}.txt`, 28 | }); 29 | } catch (e) { 30 | return; 31 | } 32 | if (!target) { 33 | return; 34 | } 35 | const progress = ProgressDialogComponent.open(this.modals); 36 | try { 37 | await writeTextFile(target, await firstValueFrom(report.content)); 38 | } finally { 39 | progress.close(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/debug/debug-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {DebugComponent} from "./debug.component"; 4 | import {CrashesComponent} from "./crashes/crashes.component"; 5 | 6 | const routes: Routes = [{ 7 | path: '', 8 | component: DebugComponent, 9 | children: [ 10 | { 11 | path: '#crashes', 12 | component: CrashesComponent 13 | } 14 | ] 15 | }]; 16 | 17 | @NgModule({ 18 | imports: [RouterModule.forChild(routes)], 19 | exports: [RouterModule] 20 | }) 21 | export class DebugRoutingModule { 22 | } 23 | -------------------------------------------------------------------------------- /src/app/debug/debug.component.html: -------------------------------------------------------------------------------- 1 |
2 | 31 | 32 |
33 |
34 | -------------------------------------------------------------------------------- /src/app/debug/debug.component.scss: -------------------------------------------------------------------------------- 1 | .debug-content { 2 | .tab-pane { 3 | height: 100% !important; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/app/debug/debug.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnDestroy, OnInit, ViewEncapsulation} from '@angular/core'; 2 | import {ActivatedRoute, Router} from "@angular/router"; 3 | import {DeviceManagerService} from "../core/services"; 4 | import {Device} from "../types"; 5 | import {Subscription} from "rxjs"; 6 | import {NgbNavChangeEvent} from "@ng-bootstrap/ng-bootstrap"; 7 | import {Location} from "@angular/common"; 8 | 9 | @Component({ 10 | selector: 'app-debug', 11 | templateUrl: './debug.component.html', 12 | styleUrls: ['./debug.component.scss'], 13 | encapsulation: ViewEncapsulation.None, 14 | }) 15 | export class DebugComponent implements OnInit, OnDestroy { 16 | device: Device | null = null; 17 | activeTab: string = 'crashes'; 18 | 19 | private subscriptions: Subscription = new Subscription(); 20 | 21 | constructor(public route: ActivatedRoute, private location: Location, private deviceManager: DeviceManagerService) { 22 | } 23 | 24 | ngOnInit(): void { 25 | this.subscriptions.add(this.deviceManager.selected$.subscribe((selected) => { 26 | this.device = selected; 27 | })); 28 | this.subscriptions.add(this.route.fragment.subscribe(frag => frag && (this.activeTab = frag))); 29 | } 30 | 31 | ngOnDestroy(): void { 32 | this.subscriptions.unsubscribe(); 33 | } 34 | 35 | tabChange($event: NgbNavChangeEvent) { 36 | $event.preventDefault(); 37 | const nextId = $event.nextId; 38 | this.activeTab = nextId; 39 | this.location.replaceState(`${this.location.path(false)}#${nextId}`); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/debug/debug.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {DebugRoutingModule} from "./debug-routing.module"; 4 | import {DebugComponent} from './debug.component'; 5 | import {NgbNavModule, NgbTooltipModule} from "@ng-bootstrap/ng-bootstrap"; 6 | import {CrashesComponent} from "./crashes/crashes.component"; 7 | import {SharedModule} from "../shared/shared.module"; 8 | import {PmLogComponent} from './pmlog/pmlog.component'; 9 | import {LogReaderComponent} from './log-reader/log-reader.component'; 10 | import {TerminalModule} from "../terminal"; 11 | import {DmesgComponent} from "./dmesg/dmesg.component"; 12 | import {PmLogControlComponent} from './pmlog/control/control.component'; 13 | import {SetContextComponent} from './pmlog/set-context/set-context.component'; 14 | import {FormsModule, ReactiveFormsModule} from "@angular/forms"; 15 | import {DetailsComponent} from './crashes/details/details.component'; 16 | import {LsMonitorComponent} from "./ls-monitor/ls-monitor.component"; 17 | import {DetailsComponent as LsMonitorDetailsComponent} from "./ls-monitor/details/details.component"; 18 | 19 | import {ObjectHighlightPipe} from "./ls-monitor/object-highlight.pipe"; 20 | 21 | import hljs from 'highlight.js' 22 | import json from 'highlight.js/lib/languages/json'; 23 | import {SearchBarDirective} from "../shared/directives"; 24 | import {SizeCalculatorComponent} from "../shared/components/term-size-calculator/size-calculator.component"; 25 | 26 | @NgModule({ 27 | declarations: [ 28 | DebugComponent, 29 | CrashesComponent, 30 | PmLogComponent, 31 | LogReaderComponent, 32 | DmesgComponent, 33 | PmLogControlComponent, 34 | SetContextComponent, 35 | DetailsComponent, 36 | LsMonitorComponent, 37 | LsMonitorDetailsComponent, 38 | ObjectHighlightPipe, 39 | ], 40 | imports: [ 41 | CommonModule, 42 | DebugRoutingModule, 43 | NgbNavModule, 44 | NgbTooltipModule, 45 | SharedModule, 46 | TerminalModule, 47 | FormsModule, 48 | ReactiveFormsModule, 49 | SearchBarDirective, 50 | SizeCalculatorComponent, 51 | ] 52 | }) 53 | export class DebugModule { 54 | constructor() { 55 | hljs.registerLanguage('json', json); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/debug/dmesg/dmesg.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 7 |
8 | 11 | 15 |
16 | 17 |
18 | -------------------------------------------------------------------------------- /src/app/debug/dmesg/dmesg.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/debug/dmesg/dmesg.component.scss -------------------------------------------------------------------------------- /src/app/debug/dmesg/dmesg.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core'; 2 | import {Device} from "../../types"; 3 | import {RemoteCommandService} from "../../core/services/remote-command.service"; 4 | import {from, identity, mergeMap, Observable} from "rxjs"; 5 | import {LogMessage, RemoteLogService} from "../../core/services/remote-log.service"; 6 | 7 | @Component({ 8 | selector: 'app-dmesg', 9 | templateUrl: './dmesg.component.html', 10 | styleUrls: ['./dmesg.component.scss'] 11 | }) 12 | export class DmesgComponent { 13 | 14 | logs?: Observable; 15 | 16 | private deviceField: Device | null = null; 17 | 18 | constructor(private cmd: RemoteCommandService, private log: RemoteLogService) { 19 | } 20 | 21 | 22 | get device(): Device | null { 23 | return this.deviceField; 24 | } 25 | 26 | @Input() 27 | set device(device: Device | null) { 28 | this.deviceField = device; 29 | this.logs = undefined; 30 | if (device) { 31 | this.reload(device); 32 | } 33 | } 34 | 35 | async clearBuffer(): Promise { 36 | const device = this.device; 37 | if (!device) { 38 | return; 39 | } 40 | await this.log.dmesgClear(device); 41 | this.reload(device); 42 | } 43 | 44 | private reload(device: Device) { 45 | this.logs = from(this.log.dmesg(device)).pipe(mergeMap(identity)); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/app/debug/index.ts: -------------------------------------------------------------------------------- 1 | export {DebugModule} from './debug.module'; 2 | -------------------------------------------------------------------------------- /src/app/debug/log-reader/log-reader.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 |
8 | 25 |
26 | -------------------------------------------------------------------------------- /src/app/debug/log-reader/log-reader.component.scss: -------------------------------------------------------------------------------- 1 | ul.pmlog { 2 | background-color: black; 3 | list-style: none; 4 | overflow-y: scroll; 5 | 6 | li.log-line { 7 | * { 8 | color: white; 9 | } 10 | 11 | &.warning * { 12 | color: orange; 13 | } 14 | 15 | &.err * { 16 | color: red; 17 | } 18 | 19 | &.debug * { 20 | color: lightgray; 21 | } 22 | 23 | &.notice * { 24 | color: limegreen; 25 | } 26 | 27 | &.crit { 28 | color: white; 29 | background-color: red; 30 | } 31 | 32 | &.alert { 33 | color: orange; 34 | background-color: red; 35 | } 36 | 37 | &.emerg { 38 | color: black; 39 | background-color: red; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/debug/ls-monitor/details/details.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 22 | 23 | 24 |
Data
15 | 16 | 17 | 18 | 19 | 20 | {{ message.information }} 21 |
25 |
26 |
27 |
28 |
Type: {{ selected.type }}
29 |
Sender: {{ selected.sender }}
30 |
Destination: {{ selected.destination }}{{ selected.methodCategory }}/{{ selected.method }}
33 |
34 |
35 |
{{ raw }}
36 |
37 |
38 | -------------------------------------------------------------------------------- /src/app/debug/ls-monitor/details/details.component.scss: -------------------------------------------------------------------------------- 1 | .message-selected { 2 | min-height: 50%; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/debug/ls-monitor/details/details.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; 2 | import {CallEntry, MonitorMessageItem} from "../ls-monitor.component"; 3 | 4 | @Component({ 5 | selector: 'app-ls-monitor-details', 6 | templateUrl: './details.component.html', 7 | styleUrls: ['../ls-monitor.component.scss', './details.component.scss'] 8 | }) 9 | export class DetailsComponent { 10 | 11 | detailsField!: CallEntry; 12 | 13 | @Output() 14 | closeClick = new EventEmitter(); 15 | 16 | selectedMessage?: MonitorMessageItem; 17 | 18 | @Input() 19 | set details(value: CallEntry) { 20 | this.detailsField = value; 21 | this.selectedMessage = value.messages[0]; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/app/debug/ls-monitor/ls-monitor.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 5 |
6 | 20 |
21 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
NameSenderInformation
{{ row.name }}{{ row.sender }}{{ row.information }}
38 |
39 |
40 | 41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /src/app/debug/ls-monitor/ls-monitor.component.scss: -------------------------------------------------------------------------------- 1 | .ls-monitor { 2 | display: grid; 3 | grid-template-rows: auto 1fr; 4 | grid-template-columns: auto 1fr; 5 | 6 | .top-bar { 7 | grid-row: 1; 8 | grid-column: 2; 9 | } 10 | 11 | .side-bar { 12 | grid-row: 1 / span 2; 13 | grid-column: 1; 14 | } 15 | 16 | .table-container { 17 | grid-row: 2; 18 | grid-column: 2; 19 | } 20 | } 21 | 22 | .table-container { 23 | align-items: start; 24 | } 25 | 26 | .details-opened { 27 | width: 23em; 28 | min-width: 23em; 29 | max-width: 23em; 30 | } 31 | 32 | 33 | table { 34 | 35 | th { 36 | position: sticky; 37 | top: 0; 38 | } 39 | 40 | th, td { 41 | font-size: small; 42 | 43 | &.name-col { 44 | width: 23em; 45 | min-width: 23em; 46 | max-width: 23em; 47 | } 48 | 49 | &.sender-col { 50 | width: 15em; 51 | min-width: 15em; 52 | max-width: 15em; 53 | } 54 | 55 | &.info-col { 56 | width: 100%; 57 | } 58 | } 59 | 60 | 61 | td.name-col { 62 | white-space: nowrap; 63 | overflow: hidden; 64 | text-overflow: ellipsis; 65 | 66 | direction: rtl; 67 | text-align: left; 68 | } 69 | 70 | td.sender-col { 71 | white-space: nowrap; 72 | overflow: hidden; 73 | text-overflow: ellipsis; 74 | 75 | direction: rtl; 76 | text-align: left; 77 | } 78 | 79 | td.info-col { 80 | white-space: nowrap; 81 | overflow: hidden; 82 | text-overflow: ellipsis; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/app/debug/ls-monitor/object-highlight.pipe.ts: -------------------------------------------------------------------------------- 1 | import {Pipe, PipeTransform} from '@angular/core'; 2 | 3 | import hljs from 'highlight.js/lib/core'; 4 | 5 | @Pipe({name: 'objectHighlight'}) 6 | export class ObjectHighlightPipe implements PipeTransform { 7 | 8 | transform(value: unknown): string { 9 | return hljs.highlight(JSON.stringify(value, undefined, 2), { 10 | language: 'json', 11 | ignoreIllegals: true 12 | }).value; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/debug/pmlog/control/control.component.html: -------------------------------------------------------------------------------- 1 | 36 | 40 | -------------------------------------------------------------------------------- /src/app/debug/pmlog/control/control.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/debug/pmlog/control/control.component.scss -------------------------------------------------------------------------------- /src/app/debug/pmlog/pmlog.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 5 | 8 |
9 | 12 | 16 |
17 | 18 |
19 | 20 |
21 | 23 |
24 |
25 | -------------------------------------------------------------------------------- /src/app/debug/pmlog/pmlog.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/debug/pmlog/pmlog.component.scss -------------------------------------------------------------------------------- /src/app/debug/pmlog/set-context/set-context.component.html: -------------------------------------------------------------------------------- 1 | 14 | 18 | -------------------------------------------------------------------------------- /src/app/debug/pmlog/set-context/set-context.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/debug/pmlog/set-context/set-context.component.scss -------------------------------------------------------------------------------- /src/app/debug/pmlog/set-context/set-context.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {NgbActiveModal, NgbModal} from "@ng-bootstrap/ng-bootstrap"; 3 | import {UntypedFormBuilder, UntypedFormGroup} from "@angular/forms"; 4 | import {LOG_LEVELS} from "../control/control.component"; 5 | import {PrefLogLevel} from "../../../core/services/remote-log.service"; 6 | 7 | @Component({ 8 | selector: 'app-pmlog-set-context', 9 | templateUrl: './set-context.component.html', 10 | styleUrls: ['./set-context.component.scss'] 11 | }) 12 | export class SetContextComponent { 13 | public formGroup: UntypedFormGroup; 14 | public logLevels = LOG_LEVELS; 15 | 16 | constructor(public modal: NgbActiveModal, fb: UntypedFormBuilder) { 17 | this.formGroup = fb.group({ 18 | context: [''], 19 | level: ['none'], 20 | }) 21 | } 22 | 23 | public static async prompt(modals: NgbModal): Promise { 24 | return modals.open(SetContextComponent).result.catch(() => undefined); 25 | } 26 | 27 | } 28 | 29 | export declare class SetContextResult { 30 | context: string; 31 | level: PrefLogLevel; 32 | } 33 | -------------------------------------------------------------------------------- /src/app/devices/devices.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Devices

3 |
    4 | @for (device of devices$ | async; track device.name) { 5 |
  • 6 |
    7 | {{ device.name }} 8 |
    9 |
    10 | @defer { 11 | 13 | } 14 |
    15 |
  • 16 | } 17 |
  • 18 | Add new device... 19 |
  • 20 |
21 |
22 | -------------------------------------------------------------------------------- /src/app/devices/devices.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/devices/devices.component.scss -------------------------------------------------------------------------------- /src/app/devices/devices.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject, Optional} from '@angular/core'; 2 | import {DeviceManagerService} from "../core/services"; 3 | import {Observable} from "rxjs"; 4 | import {Device, NewDevice} from "../types"; 5 | import {AsyncPipe} from "@angular/common"; 6 | import {NgbCollapse, NgbModal} from "@ng-bootstrap/ng-bootstrap"; 7 | import {AddDeviceModule} from "../add-device/add-device.module"; 8 | import {InlineEditorComponent} from "./inline-editor/inline-editor.component"; 9 | import {HomeComponent} from "../home/home.component"; 10 | import {RemoveConfirmation, RemoveDeviceComponent} from "../remove-device/remove-device.component"; 11 | 12 | @Component({ 13 | selector: 'app-devices', 14 | standalone: true, 15 | imports: [ 16 | AsyncPipe, 17 | AddDeviceModule, 18 | NgbCollapse, 19 | InlineEditorComponent 20 | ], 21 | templateUrl: './devices.component.html', 22 | styleUrl: './devices.component.scss' 23 | }) 24 | export class DevicesComponent { 25 | public devices$: Observable; 26 | 27 | editingDevice: Device | undefined; 28 | 29 | constructor( 30 | @Optional() @Inject(HomeComponent) public home: HomeComponent, 31 | public deviceManager: DeviceManagerService, 32 | private modals: NgbModal, 33 | ) { 34 | this.devices$ = deviceManager.devices$; 35 | } 36 | 37 | async deleteDevice(device: Device) { 38 | let answer: RemoveConfirmation; 39 | try { 40 | let a = await RemoveDeviceComponent.confirm(this.modals, device); 41 | if (!a) { 42 | return; 43 | } 44 | answer = a; 45 | } catch (e) { 46 | return; 47 | } 48 | await this.deviceManager.removeDevice(device.name, answer.deleteSshKey); 49 | this.editingDevice = undefined; 50 | } 51 | 52 | async saveDevice(device: NewDevice) { 53 | await this.deviceManager.addDevice(device); 54 | this.editingDevice = undefined; 55 | } 56 | 57 | addDevice() { 58 | this.home?.openSetupDevice(true); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/app/devices/inline-editor/inline-editor.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 9 | 12 | 15 |
16 | -------------------------------------------------------------------------------- /src/app/devices/inline-editor/inline-editor.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/devices/inline-editor/inline-editor.component.scss -------------------------------------------------------------------------------- /src/app/devices/inline-editor/inline-editor.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | import {InlineEditorComponent} from './inline-editor.component'; 4 | import {Device} from "../../types"; 5 | 6 | describe('InlineEditorComponent', async () => { 7 | let component: InlineEditorComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | imports: [InlineEditorComponent] 13 | }).compileComponents(); 14 | 15 | fixture = TestBed.createComponent(InlineEditorComponent); 16 | component = fixture.componentInstance; 17 | component.device = { 18 | name: 'test', 19 | host: '192.168.1.1', 20 | port: 22, 21 | username: 'root', 22 | profile: 'ose', 23 | privateKey: {openSsh: 'test'}, 24 | }; 25 | fixture.detectChanges(); 26 | }); 27 | 28 | it('should create', () => { 29 | expect(component).toBeTruthy(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/devices/inline-editor/inline-editor.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Input, Output} from '@angular/core'; 2 | import {AppPrivKeyName, Device, NewDevice, NewDeviceAuthentication} from "../../types"; 3 | import { 4 | DeviceEditorComponent, 5 | OpenSshLocalKeyValue, 6 | SetupAuthInfoUnion 7 | } from "../../add-device/device-editor/device-editor.component"; 8 | 9 | @Component({ 10 | selector: 'app-device-inline-editor', 11 | standalone: true, 12 | templateUrl: './inline-editor.component.html', 13 | imports: [ 14 | DeviceEditorComponent 15 | ], 16 | styleUrl: './inline-editor.component.scss' 17 | }) 18 | export class InlineEditorComponent { 19 | @Input() 20 | device!: Device; 21 | 22 | @Output() 23 | save: EventEmitter = new EventEmitter(); 24 | 25 | @Output() 26 | remove: EventEmitter = new EventEmitter(); 27 | 28 | @Output() 29 | closed: EventEmitter = new EventEmitter(); 30 | 31 | get deviceAuth(): SetupAuthInfoUnion { 32 | if (this.device.password) { 33 | return {type: NewDeviceAuthentication.Password, value: this.device.password}; 34 | } else if (this.device.username === 'prisoner') { 35 | return { 36 | type: NewDeviceAuthentication.DevKey, 37 | value: this.device.passphrase! 38 | }; 39 | } else if (this.device.privateKey?.openSsh === AppPrivKeyName) { 40 | return { 41 | type: NewDeviceAuthentication.AppKey, 42 | value: null, 43 | }; 44 | } else { 45 | return { 46 | type: NewDeviceAuthentication.LocalKey, 47 | value: new OpenSshLocalKeyValue(this.device.privateKey!.openSsh, this.device.passphrase), 48 | } 49 | } 50 | } 51 | 52 | doSave(editor: DeviceEditorComponent) { 53 | editor.submit().then(dev => this.save.emit(dev)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/files/attrs-permissions.pipe.ts: -------------------------------------------------------------------------------- 1 | import {Pipe, PipeTransform} from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'attrsPermissions' 5 | }) 6 | export class AttrsPermissionsPipe implements PipeTransform { 7 | 8 | transform(mode: number): string { 9 | return `${str((mode >> 6) & 7)}${str((mode >> 3) & 7)}${str(mode & 7)}`; 10 | } 11 | 12 | } 13 | 14 | function str(bits: number): string { 15 | return `${bits & 4 ? 'r' : '-'}${bits & 2 ? 'w' : '-'}${bits & 1 ? 'x' : '-'}`; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/files/create-directory-message/create-directory-message.component.html: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /src/app/files/create-directory-message/create-directory-message.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/files/create-directory-message/create-directory-message.component.scss -------------------------------------------------------------------------------- /src/app/files/create-directory-message/create-directory-message.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {MessageDialogComponent} from "../../shared/components/message-dialog/message-dialog.component"; 3 | import {FormControl, Validators} from "@angular/forms"; 4 | 5 | @Component({ 6 | selector: 'app-create-directory-message', 7 | templateUrl: './create-directory-message.component.html', 8 | styleUrls: ['./create-directory-message.component.scss'] 9 | }) 10 | export class CreateDirectoryMessageComponent { 11 | public formControl: FormControl; 12 | 13 | constructor(private parent: MessageDialogComponent) { 14 | this.formControl = new FormControl('', { 15 | nonNullable: true, 16 | validators: [ 17 | Validators.required, 18 | Validators.pattern(/^[^\\/:*?"<>|]+$/), 19 | Validators.pattern(/[^.]$/) 20 | ] 21 | }); 22 | this.formControl.statusChanges.subscribe((v) => { 23 | parent.positiveDisabled = v !== 'VALID'; 24 | }); 25 | parent.positiveDisabled = this.formControl.invalid; 26 | parent.positiveAction = () => this.formControl.value; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/files/files-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {FilesComponent} from "./files.component"; 4 | 5 | const routes: Routes = [{ 6 | path: '', 7 | component: FilesComponent, 8 | }]; 9 | 10 | @NgModule({ 11 | imports: [RouterModule.forChild(routes)], 12 | exports: [RouterModule] 13 | }) 14 | export class FilesRoutingModule { 15 | } 16 | -------------------------------------------------------------------------------- /src/app/files/files-table/files-table.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 32 | 34 | 40 | 41 | 44 | 47 | 48 | 49 |
NameLast ModifiedSizePermissionOwnerGroup
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {{ file.filename }} 27 | 28 | 29 | {{ link.target ?? '?' }} 30 | 31 | {{ file.mtime * 1000 | date:'short' }} 33 | 35 | 36 | {{ file.size | filesize:sizeOptions }} 37 | 38 | - 39 | {{ file.mode }} 42 | {{ file.user }} 43 | 45 | {{ file.group }} 46 |
50 | - 51 | -------------------------------------------------------------------------------- /src/app/files/files-table/files-table.component.scss: -------------------------------------------------------------------------------- 1 | table { 2 | th { 3 | position: sticky; 4 | top: 0; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/app/files/files.component.scss: -------------------------------------------------------------------------------- 1 | .breadcrumb-bar { 2 | nav { 3 | width: fit-content; 4 | --bs-breadcrumb-divider: ''; 5 | } 6 | } 7 | 8 | .stat-bar { 9 | .breadcrumb { 10 | flex-wrap: nowrap; 11 | align-items: center; 12 | } 13 | 14 | .breadcrumb-item { 15 | position: relative; 16 | } 17 | } 18 | 19 | .loading, .loading * { 20 | cursor: progress; 21 | } 22 | -------------------------------------------------------------------------------- /src/app/files/files.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | 4 | import {FilesRoutingModule} from './files-routing.module'; 5 | import {FilesComponent} from "./files.component"; 6 | import {AttrsPermissionsPipe} from "./attrs-permissions.pipe"; 7 | import { 8 | NgbDropdown, 9 | NgbDropdownItem, 10 | NgbDropdownMenu, 11 | NgbDropdownToggle, 12 | NgbTooltipModule 13 | } from "@ng-bootstrap/ng-bootstrap"; 14 | import {FilesTableComponent} from './files-table/files-table.component'; 15 | import {SharedModule} from "../shared/shared.module"; 16 | import {CreateDirectoryMessageComponent} from './create-directory-message/create-directory-message.component'; 17 | import {ReactiveFormsModule} from "@angular/forms"; 18 | 19 | 20 | @NgModule({ 21 | declarations: [ 22 | FilesComponent, 23 | AttrsPermissionsPipe, 24 | FilesTableComponent, 25 | CreateDirectoryMessageComponent, 26 | ], 27 | imports: [ 28 | CommonModule, 29 | FilesRoutingModule, 30 | NgbTooltipModule, 31 | SharedModule, 32 | NgbDropdown, 33 | NgbDropdownItem, 34 | NgbDropdownMenu, 35 | NgbDropdownToggle, 36 | ReactiveFormsModule, 37 | ] 38 | }) 39 | export class FilesModule { 40 | } 41 | -------------------------------------------------------------------------------- /src/app/files/index.ts: -------------------------------------------------------------------------------- 1 | export {FilesModule} from './files.module'; 2 | -------------------------------------------------------------------------------- /src/app/home/device-chooser/device-chooser.component.html: -------------------------------------------------------------------------------- 1 | 4 |
    5 |
  • 7 | {{ device.name }} 8 |
  • 9 |
10 | -------------------------------------------------------------------------------- /src/app/home/device-chooser/device-chooser.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/home/device-chooser/device-chooser.component.scss -------------------------------------------------------------------------------- /src/app/home/device-chooser/device-chooser.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap"; 3 | import {DeviceManagerService} from "../../core/services"; 4 | import {AsyncPipe, NgForOf} from "@angular/common"; 5 | 6 | @Component({ 7 | selector: 'app-device-chooser', 8 | standalone: true, 9 | imports: [ 10 | AsyncPipe, 11 | NgForOf 12 | ], 13 | templateUrl: './device-chooser.component.html', 14 | styleUrl: './device-chooser.component.scss' 15 | }) 16 | export class DeviceChooserComponent { 17 | constructor( 18 | public modal: NgbActiveModal, 19 | public deviceManager: DeviceManagerService, 20 | ) { 21 | 22 | } 23 | 24 | protected readonly parent = parent; 25 | } 26 | -------------------------------------------------------------------------------- /src/app/home/home.component.scss: -------------------------------------------------------------------------------- 1 | .nav { 2 | .nav-item { 3 | margin: 5px; 4 | 5 | .nav-link { 6 | padding: 5px 12px; 7 | i.bi { 8 | font-size: 30px; 9 | } 10 | } 11 | } 12 | 13 | .device-check { 14 | width: 1em; 15 | } 16 | 17 | .dropdown-toggle:after { 18 | display: none; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/home/nav-more/nav-more.component.html: -------------------------------------------------------------------------------- 1 |
2 |
Device tools
3 | 8 |
Settings
9 | 14 |
About this app
15 | 25 |
26 | -------------------------------------------------------------------------------- /src/app/home/nav-more/nav-more.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/home/nav-more/nav-more.component.scss -------------------------------------------------------------------------------- /src/app/home/nav-more/nav-more.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {ActivatedRoute, RouterLink} from "@angular/router"; 3 | import {HomeComponent} from "../home.component"; 4 | import ReleaseInfo from '../../../release.json'; 5 | import {SharedModule} from "../../shared/shared.module"; 6 | import {ExternalLinkDirective} from "../../shared/directives"; 7 | 8 | @Component({ 9 | selector: 'app-nav-more', 10 | standalone: true, 11 | imports: [ 12 | RouterLink, 13 | SharedModule, 14 | ExternalLinkDirective 15 | ], 16 | templateUrl: './nav-more.component.html', 17 | styleUrl: './nav-more.component.scss' 18 | }) 19 | export class NavMoreComponent { 20 | homeRoute: ActivatedRoute | null; 21 | readonly appVersion: string; 22 | 23 | constructor( 24 | public route: ActivatedRoute, 25 | public parent: HomeComponent, 26 | ) { 27 | this.homeRoute = route.parent; 28 | this.appVersion = ReleaseInfo.version; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/info/devmode-countdown.pipe.ts: -------------------------------------------------------------------------------- 1 | import {Pipe, PipeTransform} from '@angular/core'; 2 | import {Observable, of, timer} from "rxjs"; 3 | import {DateTime, Duration, DurationLikeObject} from "luxon"; 4 | import {map} from "rxjs/operators"; 5 | 6 | @Pipe({ 7 | name: 'devmodeCountdown' 8 | }) 9 | export class DevmodeCountdownPipe implements PipeTransform { 10 | 11 | transform(value?: string): Observable { 12 | const remainingMatches = RegExp(/^(?\d+):(?\d+):(?\d+)$/) 13 | .exec(value ?? ''); 14 | if (remainingMatches) { 15 | const expireDate = DateTime.now().plus(Duration.fromDurationLike(remainingMatches.groups as 16 | Pick)); 17 | return timer(0, 1000).pipe(map(() => expireDate 18 | .diffNow('seconds').toFormat('hh:mm:ss'))); 19 | } else { 20 | return of("--:--"); 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/app/info/index.ts: -------------------------------------------------------------------------------- 1 | export {InfoModule} from './info.module'; 2 | -------------------------------------------------------------------------------- /src/app/info/info-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {InfoComponent} from "./info.component"; 4 | 5 | const routes: Routes = [{ 6 | path: '', 7 | component: InfoComponent, 8 | }]; 9 | 10 | @NgModule({ 11 | imports: [RouterModule.forChild(routes)], 12 | exports: [RouterModule] 13 | }) 14 | export class InfoRoutingModule { 15 | } 16 | -------------------------------------------------------------------------------- /src/app/info/info.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/info/info.component.scss -------------------------------------------------------------------------------- /src/app/info/info.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | 4 | import {InfoRoutingModule} from './info-routing.module'; 5 | import {InfoComponent} from "./info.component"; 6 | import {SharedModule} from "../shared/shared.module"; 7 | import {DevmodeCountdownPipe} from './devmode-countdown.pipe'; 8 | import {NgbDropdownModule} from "@ng-bootstrap/ng-bootstrap"; 9 | import {FormsModule} from "@angular/forms"; 10 | import {ExternalLinkDirective} from "../shared/directives"; 11 | 12 | 13 | @NgModule({ 14 | declarations: [ 15 | InfoComponent, 16 | DevmodeCountdownPipe, 17 | ], 18 | imports: [ 19 | CommonModule, 20 | InfoRoutingModule, 21 | SharedModule, 22 | NgbDropdownModule, 23 | FormsModule, 24 | ExternalLinkDirective, 25 | ] 26 | }) 27 | export class InfoModule { 28 | } 29 | -------------------------------------------------------------------------------- /src/app/info/renew-script/renew-script.component.scss: -------------------------------------------------------------------------------- 1 | pre, code { 2 | white-space: pre-wrap; 3 | user-select: all; 4 | cursor: text; 5 | } 6 | 7 | // TODO: Remove after BS 5 8 | .top-0 { 9 | top: 0 !important; 10 | } 11 | 12 | .right-0 { 13 | right: 0 !important; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/info/renew-script/renew-script.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject, OnInit} from '@angular/core'; 2 | import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; 3 | import {Device} from '../../types'; 4 | import {DeviceManagerService, DevModeStatus} from '../../core/services'; 5 | import {noop} from 'rxjs'; 6 | import {save as showSaveDialog} from '@tauri-apps/plugin-dialog' 7 | import {writeTextFile} from '@tauri-apps/plugin-fs'; 8 | import renewScriptTemplate from './renew-script.sh'; 9 | import Mustache from 'mustache'; 10 | 11 | @Component({ 12 | selector: 'app-renew-script', 13 | templateUrl: './renew-script.component.html', 14 | styleUrls: ['./renew-script.component.scss'] 15 | }) 16 | export class RenewScriptComponent implements OnInit { 17 | 18 | public renewScriptContent?: string; 19 | 20 | constructor( 21 | public modal: NgbActiveModal, 22 | private deviceManager: DeviceManagerService, 23 | @Inject('device') public device: Device, 24 | @Inject('devMode') public devMode: DevModeStatus, 25 | ) { 26 | } 27 | 28 | ngOnInit(): void { 29 | this.deviceManager.readPrivKey(this.device).then(key => { 30 | this.renewScriptContent = Mustache.render(renewScriptTemplate, { 31 | device: this.device, 32 | keyContent: key.trim(), 33 | }, undefined, { 34 | escape: (v) => v, 35 | }); 36 | }); 37 | } 38 | 39 | async copyScript(content: string): Promise { 40 | await navigator.clipboard.writeText(content); 41 | } 42 | 43 | saveScript(content: string): void { 44 | showSaveDialog({ 45 | defaultPath: `renew-devmode-${this.device.name}.sh` 46 | }).then(value => { 47 | if (!value) { 48 | return; 49 | } 50 | return writeTextFile(value, content); 51 | }).catch(noop); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/info/renew-script/renew-script.sh.ts: -------------------------------------------------------------------------------- 1 | // language=shell 2 | export default `#!/bin/sh 3 | 4 | # WARNING: Do not run this as root! 5 | # If $SESSION_TOKEN_CACHE is a symlink, its target wil be overwritten. 6 | 7 | DEVICE_NAME='{{device.name}}' 8 | DEVICE_HOST='{{device.host}}' 9 | DEVICE_PORT='{{device.port}}' 10 | DEVICE_USERNAME='{{device.username}}' 11 | DEVICE_PASSPHRASE='{{device.passphrase}}' 12 | 13 | umask 077 14 | 15 | if ! TEMP_KEY_DIR="$(mktemp -d)"; then 16 | echo "Failed to create random temporary directory for key; using fallback" >&2 17 | TEMP_KEY_DIR="/tmp/renew-script.$$" 18 | if ! mkdir "\${TEMP_KEY_DIR}"; then 19 | echo "Fallback temporary directory \${TEMP_KEY_DIR} already exists" >&2 20 | exit 1 21 | fi 22 | fi 23 | 24 | PRIV_KEY_FILE="\${TEMP_KEY_DIR}/webos_privkey_\${DEVICE_NAME}" 25 | 26 | cat >"\${PRIV_KEY_FILE}" <&2 47 | SESSION_TOKEN=$(cat "\${SESSION_TOKEN_CACHE}") 48 | else 49 | echo "Got SESSION_TOKEN from TV - writing to \${SESSION_TOKEN_CACHE}" >&2 50 | echo "$SESSION_TOKEN" >"\${SESSION_TOKEN_CACHE}" 51 | fi 52 | 53 | if [ -z "$SESSION_TOKEN" ]; then 54 | echo "Unable to get token" >&2 55 | exit 1 56 | fi 57 | 58 | CHECK_RESULT=$(curl --max-time 3 -s "https://developer.lge.com/secure/ResetDevModeSession.dev?sessionToken=$SESSION_TOKEN") 59 | 60 | echo "\${CHECK_RESULT}"`; 61 | -------------------------------------------------------------------------------- /src/app/remove-device/remove-device.component.html: -------------------------------------------------------------------------------- 1 | 4 | 13 | 17 | -------------------------------------------------------------------------------- /src/app/remove-device/remove-device.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/remove-device/remove-device.component.scss -------------------------------------------------------------------------------- /src/app/remove-device/remove-device.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject, Injector} from '@angular/core'; 2 | import {NgbActiveModal, NgbModal} from "@ng-bootstrap/ng-bootstrap"; 3 | import {Device} from "../types"; 4 | 5 | @Component({ 6 | selector: 'app-remove-device', 7 | templateUrl: './remove-device.component.html', 8 | styleUrls: ['./remove-device.component.scss'] 9 | }) 10 | export class RemoveDeviceComponent { 11 | 12 | public deleteSshKey: boolean = false; 13 | 14 | constructor(@Inject('device') public device: Device, public modal: NgbActiveModal) { 15 | } 16 | 17 | get canDeleteSshKey(): boolean { 18 | return this.device.privateKey?.openSsh?.startsWith("webos_") === true; 19 | } 20 | 21 | confirmDeletion() { 22 | this.modal.close({ 23 | deleteSshKey: this.deleteSshKey, 24 | }); 25 | } 26 | 27 | static confirm(service: NgbModal, device: Device): Promise { 28 | return service.open(RemoveDeviceComponent, { 29 | centered: true, 30 | size: 'lg', 31 | scrollable: true, 32 | injector: Injector.create({ 33 | providers: [{provide: 'device', useValue: device}] 34 | }) 35 | }).result.catch(() => null); 36 | } 37 | } 38 | 39 | export interface RemoveConfirmation { 40 | deleteSshKey: boolean; 41 | } 42 | -------------------------------------------------------------------------------- /src/app/shared/components/error-card/error-card.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | @if (title) { 4 |
{{ title }}
5 | } 6 |

{{ error.message }}

7 | @let details = $any(error).details; 8 | @if (details) { 9 |
{{ details }}
10 | } 11 | 12 |
13 |
14 | -------------------------------------------------------------------------------- /src/app/shared/components/error-card/error-card.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/shared/components/error-card/error-card.component.scss -------------------------------------------------------------------------------- /src/app/shared/components/error-card/error-card.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Input, Output} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-error-card', 5 | templateUrl: './error-card.component.html', 6 | styleUrls: ['./error-card.component.scss'] 7 | }) 8 | export class ErrorCardComponent { 9 | @Input() 10 | title?: string; 11 | 12 | @Input() 13 | error!: Error; 14 | 15 | @Output() 16 | retry: EventEmitter = new EventEmitter(); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/app/shared/components/loading-card/loading-card.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Loading... 4 |
5 |
6 | -------------------------------------------------------------------------------- /src/app/shared/components/loading-card/loading-card.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/shared/components/loading-card/loading-card.component.scss -------------------------------------------------------------------------------- /src/app/shared/components/loading-card/loading-card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-loading-card', 5 | templateUrl: './loading-card.component.html', 6 | styleUrls: ['./loading-card.component.scss'] 7 | }) 8 | export class LoadingCardComponent { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/app/shared/components/message-dialog/message-dialog.component.html: -------------------------------------------------------------------------------- 1 | @if (title) { 2 | 5 | } 6 | 23 | 42 | -------------------------------------------------------------------------------- /src/app/shared/components/message-dialog/message-dialog.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/shared/components/message-dialog/message-dialog.component.scss -------------------------------------------------------------------------------- /src/app/shared/components/message-dialog/message-trace/message-trace.component.html: -------------------------------------------------------------------------------- 1 |

2 | {{ message }} 3 | 4 | Details 5 | 6 |

7 | 8 |
9 |
10 | {{ error }} 11 |
12 |
13 | -------------------------------------------------------------------------------- /src/app/shared/components/message-dialog/message-trace/message-trace.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/shared/components/message-dialog/message-trace/message-trace.component.scss -------------------------------------------------------------------------------- /src/app/shared/components/message-dialog/message-trace/message-trace.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-message-trace', 5 | templateUrl: './message-trace.component.html', 6 | styleUrls: ['./message-trace.component.scss'] 7 | }) 8 | export class MessageTraceComponent { 9 | 10 | message: string = ''; 11 | error: any; 12 | detailsCollapsed: boolean = true; 13 | 14 | constructor() { 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/app/shared/components/page-not-found/page-not-found.component.html: -------------------------------------------------------------------------------- 1 |

2 | page-not-found works! 3 |

4 | -------------------------------------------------------------------------------- /src/app/shared/components/page-not-found/page-not-found.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/shared/components/page-not-found/page-not-found.component.scss -------------------------------------------------------------------------------- /src/app/shared/components/page-not-found/page-not-found.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-page-not-found', 5 | templateUrl: './page-not-found.component.html', 6 | styleUrls: ['./page-not-found.component.scss'] 7 | }) 8 | export class PageNotFoundComponent { 9 | constructor() { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/shared/components/progress-dialog/progress-dialog.component.html: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /src/app/shared/components/progress-dialog/progress-dialog.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/shared/components/progress-dialog/progress-dialog.component.scss -------------------------------------------------------------------------------- /src/app/shared/components/progress-dialog/progress-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, NgZone} from '@angular/core'; 2 | import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; 3 | 4 | @Component({ 5 | selector: 'app-progress-dialog', 6 | templateUrl: './progress-dialog.component.html', 7 | styleUrls: ['./progress-dialog.component.scss'] 8 | }) 9 | export class ProgressDialogComponent { 10 | 11 | message?: string; 12 | progress?: number; 13 | 14 | secondaryProgress?: number; 15 | 16 | protected readonly isNaN = isNaN; 17 | 18 | constructor(private zone: NgZone) { 19 | } 20 | 21 | update(message?: string, progress?: number): void { 22 | this.zone.run(() => { 23 | this.message = message; 24 | this.progress = progress; 25 | }); 26 | } 27 | 28 | updateSecondary(message?: string, progress?: number): void { 29 | this.zone.run(() => { 30 | this.secondaryProgress = progress; 31 | }); 32 | } 33 | 34 | static open(service: NgbModal): NgbModalRef { 35 | return service.open(ProgressDialogComponent, { 36 | centered: true, 37 | backdrop: 'static', 38 | keyboard: false 39 | }); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/app/shared/components/stat-storage-info/stat-storage-info.component.html: -------------------------------------------------------------------------------- 1 | @if (storage) { 2 |
4 |
5 | 6 | {{ storage.available * 1024 | filesize:sizeOptions }} / {{ storage.total * 1024 | filesize:sizeOptions }} free 7 | 8 |
9 |
11 | 12 | {{ storage.available * 1024 | filesize:sizeOptions }} / {{ storage.total * 1024 | filesize:sizeOptions }} free 13 | 14 |
15 |
16 | } 17 | -------------------------------------------------------------------------------- /src/app/shared/components/stat-storage-info/stat-storage-info.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | position: relative; 3 | } 4 | 5 | small { 6 | font-size: xx-small; 7 | } 8 | 9 | .progress { 10 | height: 1.25em; 11 | } 12 | 13 | .progress-bar { 14 | z-index: 10; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/shared/components/stat-storage-info/stat-storage-info.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core'; 2 | import {Device, StorageInfo} from "../../../types"; 3 | import {DeviceManagerService} from "../../../core/services"; 4 | import {FileSizeOptionsBase} from "filesize"; 5 | 6 | @Component({ 7 | selector: 'app-stat-storage-info', 8 | templateUrl: './stat-storage-info.component.html', 9 | styleUrls: ['./stat-storage-info.component.scss'] 10 | }) 11 | export class StatStorageInfoComponent { 12 | 13 | private deviceField: Device | null = null; 14 | private locationField: string | null = null; 15 | 16 | storage: StorageInfo | null = null; 17 | 18 | sizeOptions: FileSizeOptionsBase = {round: 0, standard: "jedec"}; 19 | 20 | constructor(private service: DeviceManagerService) { 21 | 22 | } 23 | 24 | get device(): Device | null { 25 | return this.deviceField; 26 | } 27 | 28 | @Input() 29 | set device(value: Device | null) { 30 | const changed = this.deviceField !== value; 31 | this.deviceField = value; 32 | if (changed) { 33 | this.storage = null; 34 | this.refresh(); 35 | } 36 | } 37 | 38 | get location(): string | null { 39 | return this.locationField; 40 | } 41 | 42 | @Input() 43 | set location(value: string | null) { 44 | const changed = this.locationField !== value; 45 | this.locationField = value; 46 | if (changed) { 47 | this.storage = null; 48 | this.refresh(); 49 | } 50 | } 51 | 52 | public refresh(): void { 53 | const device = this.deviceField; 54 | if (!device) { 55 | return; 56 | } 57 | this.service.getStorageInfo(device, this.locationField || undefined) 58 | .catch(() => null).then(info => { 59 | if (!info) { 60 | return; 61 | } 62 | this.storage = info; 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/app/shared/components/term-size-calculator/size-calculator.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | -------------------------------------------------------------------------------- /src/app/shared/components/term-size-calculator/size-calculator.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/shared/components/term-size-calculator/size-calculator.component.scss -------------------------------------------------------------------------------- /src/app/shared/constants.ts: -------------------------------------------------------------------------------- 1 | 2 | export const APP_ID_HBCHANNEL = 'org.webosbrew.hbchannel'; 3 | -------------------------------------------------------------------------------- /src/app/shared/directives/external-link.directive.ts: -------------------------------------------------------------------------------- 1 | import {Directive, HostListener} from '@angular/core'; 2 | import {open} from "@tauri-apps/plugin-shell"; 3 | import {noop} from "rxjs"; 4 | 5 | @Directive({ 6 | selector: '[appExternalLink]', 7 | standalone: true 8 | }) 9 | export class ExternalLinkDirective { 10 | 11 | @HostListener('click', ['$event']) 12 | onClick(e: Event): boolean { 13 | const href = (e.currentTarget as HTMLAnchorElement)?.href; 14 | if (!href) { 15 | return false; 16 | } 17 | if (open && this.isLinkExternal(href)) { 18 | open(href).then(noop); 19 | return false; 20 | } else { 21 | window.open(href, '_blank'); 22 | return false; 23 | } 24 | } 25 | 26 | private isLinkExternal(link: string) { 27 | const url = new URL(link); 28 | if (location.protocol == 'file:' && url.protocol != location.protocol) return true; 29 | return !url.hostname.endsWith(location.hostname); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/shared/directives/index.ts: -------------------------------------------------------------------------------- 1 | export * from './external-link.directive'; 2 | export * from './search-bar.directive'; 3 | -------------------------------------------------------------------------------- /src/app/shared/directives/search-bar.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import {SearchBarDirective} from './search-bar.directive'; 2 | import {Component} from "@angular/core"; 3 | import {TestBed} from "@angular/core/testing"; 4 | 5 | describe('SearchBarDirective', () => { 6 | 7 | it('should create an instance', () => { 8 | let component = TestBed.createComponent(TestSearchBarDirectiveHostComponent); 9 | }); 10 | }); 11 | 12 | @Component({ 13 | selector: 'app-test-search-bar-directive-host', 14 | template: `` 16 | }) 17 | class TestSearchBarDirectiveHostComponent { 18 | } 19 | -------------------------------------------------------------------------------- /src/app/shared/directives/search-bar.directive.ts: -------------------------------------------------------------------------------- 1 | import {Directive, ElementRef, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output} from '@angular/core'; 2 | import {parse as parseQuery, SearchParserResult} from 'search-query-parser'; 3 | import {debounceTime, Subject, Subscription} from "rxjs"; 4 | 5 | @Directive({ 6 | selector: 'input[appSearchBar]', 7 | standalone: true, 8 | }) 9 | export class SearchBarDirective implements OnInit, OnDestroy { 10 | 11 | @Output() 12 | query: EventEmitter = new EventEmitter(); 13 | 14 | private keywordsField: string[] = []; 15 | private rangesField: string[] = []; 16 | private emitChanges: Subject = new Subject(); 17 | private changesSubscription!: Subscription; 18 | 19 | constructor(private hostRef: ElementRef) { 20 | } 21 | 22 | 23 | @Input() 24 | set keywords(value: string | undefined) { 25 | this.keywordsField = value?.split(',') || []; 26 | this.emitChanges.next(); 27 | } 28 | 29 | @Input() 30 | set ranges(value: string | undefined) { 31 | this.rangesField = value?.split(',') || []; 32 | this.emitChanges.next(); 33 | } 34 | 35 | ngOnInit(): void { 36 | this.changesSubscription = this.emitChanges.pipe(debounceTime(50)).subscribe(() => this.emitChange()); 37 | this.emitChanges.next(); 38 | } 39 | 40 | ngOnDestroy() { 41 | this.changesSubscription.unsubscribe(); 42 | this.emitChanges.complete(); 43 | } 44 | 45 | @HostListener('change') 46 | inputChanged(): void { 47 | this.emitChanges.next(); 48 | } 49 | 50 | private emitChange(): void { 51 | this.query.emit(parseQuery(this.hostRef.nativeElement?.value || '', { 52 | keywords: this.keywordsField, 53 | ranges: this.rangesField, 54 | alwaysArray: true, 55 | tokenize: true, 56 | })); 57 | } 58 | 59 | } 60 | 61 | 62 | export type TokenizedSearchParserResult = SearchParserResult & { text?: string[] }; 63 | -------------------------------------------------------------------------------- /src/app/shared/operators.ts: -------------------------------------------------------------------------------- 1 | export function isNonNull(value: T): value is NonNullable { 2 | return value != null; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/shared/pipes/filesize.pipe.ts: -------------------------------------------------------------------------------- 1 | import {Pipe, PipeTransform} from "@angular/core"; 2 | import {filesize, FileSizeOptionsBase} from 'filesize'; 3 | 4 | @Pipe({ 5 | name: 'filesize' 6 | }) 7 | export class FilesizePipe implements PipeTransform { 8 | 9 | transform(bytes: number, options: Partial): string { 10 | return filesize(bytes, {output: "string", ...options}); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/app/shared/pipes/trust-uri.pipe.ts: -------------------------------------------------------------------------------- 1 | import {Pipe, PipeTransform} from "@angular/core"; 2 | import {DomSanitizer} from '@angular/platform-browser'; 3 | 4 | @Pipe({ 5 | name: 'trustUri' 6 | }) 7 | export class TrustUriPipe implements PipeTransform { 8 | 9 | constructor(private sanitizer: DomSanitizer) { 10 | } 11 | 12 | transform(uri?: string) { 13 | return uri && this.sanitizer.bypassSecurityTrustUrl(uri); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import {CommonModule} from '@angular/common'; 2 | import {NgModule} from '@angular/core'; 3 | import {FormsModule} from '@angular/forms'; 4 | import {PageNotFoundComponent} from './components/page-not-found/page-not-found.component'; 5 | import {TrustUriPipe} from './pipes/trust-uri.pipe'; 6 | import {MessageDialogComponent} from './components/message-dialog/message-dialog.component'; 7 | import {ProgressDialogComponent} from './components/progress-dialog/progress-dialog.component'; 8 | import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; 9 | import {MessageTraceComponent} from './components/message-dialog/message-trace/message-trace.component'; 10 | import {ErrorCardComponent} from './components/error-card/error-card.component'; 11 | import {ExternalLinkDirective} from "./directives"; 12 | import {LoadingCardComponent} from './components/loading-card/loading-card.component'; 13 | import {StatStorageInfoComponent} from './components/stat-storage-info/stat-storage-info.component'; 14 | import {FilesizePipe} from "./pipes/filesize.pipe"; 15 | 16 | @NgModule({ 17 | declarations: [ 18 | PageNotFoundComponent, 19 | TrustUriPipe, 20 | FilesizePipe, 21 | MessageDialogComponent, 22 | ProgressDialogComponent, 23 | MessageTraceComponent, 24 | ErrorCardComponent, 25 | LoadingCardComponent, 26 | StatStorageInfoComponent, 27 | ], 28 | imports: [CommonModule, FormsModule, NgbModule, 29 | ExternalLinkDirective], 30 | exports: [ 31 | PageNotFoundComponent, 32 | TrustUriPipe, 33 | FilesizePipe, 34 | MessageDialogComponent, 35 | ProgressDialogComponent, 36 | MessageTraceComponent, 37 | ErrorCardComponent, 38 | LoadingCardComponent, 39 | StatStorageInfoComponent 40 | ] 41 | }) 42 | export class SharedModule { 43 | } 44 | -------------------------------------------------------------------------------- /src/app/shared/xterm/config.ts: -------------------------------------------------------------------------------- 1 | import {ITerminalOptions} from "@xterm/xterm"; 2 | 3 | export const TERMINAL_CONFIG: Partial = { 4 | fontFamily: 'monospace', 5 | }; 6 | -------------------------------------------------------------------------------- /src/app/shared/xterm/web-links.ts: -------------------------------------------------------------------------------- 1 | import {WebLinksAddon} from "@xterm/addon-web-links"; 2 | import {open} from "@tauri-apps/plugin-shell"; 3 | import {noop} from "rxjs"; 4 | 5 | export class AppWebLinksAddon extends WebLinksAddon { 6 | constructor() { 7 | super((event, uri) => { 8 | event.preventDefault(); 9 | open(uri).then(noop); 10 | }); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/terminal/dumb/dumb.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Interactive terminal is not supported for this connection. Check here for 5 | more info. 6 |
7 | @for (item of logs; track item.id) { 8 |
9 |
10 | 11 |
{{ item.input }}
12 |
13 |
14 | 15 |
{{ item.output }}
16 |
17 |
18 |
19 | } 20 |
21 | 22 | 26 | @if (working) { 27 |
28 | Executing... 29 |
30 | } 31 |
32 |
33 | -------------------------------------------------------------------------------- /src/app/terminal/dumb/dumb.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | font-family: monospace; 3 | color: white; 4 | overflow-y: scroll; 5 | } 6 | 7 | .logs { 8 | } 9 | 10 | .prompt { 11 | textarea { 12 | resize: none; 13 | background: none; 14 | border: none; 15 | font-size: 0.875rem; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/terminal/index.ts: -------------------------------------------------------------------------------- 1 | export {TerminalModule} from './terminal.module'; 2 | -------------------------------------------------------------------------------- /src/app/terminal/pty/pty.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | -------------------------------------------------------------------------------- /src/app/terminal/pty/pty.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/app/terminal/pty/pty.component.scss -------------------------------------------------------------------------------- /src/app/terminal/terminal-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {TerminalComponent} from "./terminal.component"; 4 | 5 | const routes: Routes = [{ 6 | path: '', 7 | component: TerminalComponent 8 | }]; 9 | 10 | @NgModule({ 11 | imports: [RouterModule.forChild(routes)], 12 | exports: [RouterModule] 13 | }) 14 | export class TerminalRoutingModule { 15 | } 16 | -------------------------------------------------------------------------------- /src/app/terminal/terminal.component.scss: -------------------------------------------------------------------------------- 1 | .tabs-container { 2 | background-color: black; 3 | } 4 | 5 | ul.terminal-tabs { 6 | overflow-y: hidden; 7 | overflow-x: scroll; 8 | -webkit-overflow-scrolling: touch; 9 | &::-webkit-scrollbar { 10 | display: none; 11 | } 12 | } 13 | 14 | .terminal-tab-page { 15 | position: absolute; 16 | margin: auto; 17 | left: 0; 18 | top: 0; 19 | right: 0; 20 | bottom: 0; 21 | } 22 | 23 | .terminal-closed { 24 | 25 | } 26 | 27 | .terminal-placeholder { 28 | .xterm { 29 | height: 100% !important; 30 | 31 | .xterm-viewport { 32 | width: 100% !important; 33 | height: 100% !important; 34 | } 35 | } 36 | } 37 | 38 | .terminal-resize { 39 | position: absolute; 40 | margin: auto; 41 | top: 0; 42 | bottom: 0; 43 | left: 0; 44 | right: 0; 45 | width: fit-content; 46 | height: min-content; 47 | padding: 5px 10px; 48 | 49 | color: white; 50 | background-color: rgba($color: #000000, $alpha: 0.5); 51 | border-radius: 4px; 52 | } 53 | -------------------------------------------------------------------------------- /src/app/terminal/terminal.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | 4 | import {TerminalRoutingModule} from './terminal-routing.module'; 5 | import {NgbDropdownModule, NgbNavModule} from "@ng-bootstrap/ng-bootstrap"; 6 | import {TerminalComponent} from "./terminal.component"; 7 | import {PtyComponent} from "./pty/pty.component"; 8 | import {FormsModule} from "@angular/forms"; 9 | import {SharedModule} from "../shared/shared.module"; 10 | import {AutosizeModule} from "ngx-autosize"; 11 | import {DumbComponent} from "./dumb/dumb.component"; 12 | 13 | 14 | @NgModule({ 15 | imports: [ 16 | CommonModule, 17 | NgbNavModule, 18 | DumbComponent, 19 | PtyComponent, 20 | TerminalComponent, 21 | TerminalRoutingModule, 22 | AutosizeModule, 23 | NgbDropdownModule, 24 | FormsModule, 25 | ] 26 | }) 27 | export class TerminalModule { 28 | } 29 | -------------------------------------------------------------------------------- /src/app/types/device-manager.ts: -------------------------------------------------------------------------------- 1 | import {Device} from "./device"; 2 | 3 | 4 | export declare interface CrashReportEntry { 5 | device: Device; 6 | path: string; 7 | } 8 | 9 | 10 | export declare interface DevicePrivateKey { 11 | data: string; 12 | privatePEM?: string; 13 | } 14 | 15 | export declare interface RawPackageInfo { 16 | id: string; 17 | type: string; 18 | title: string; 19 | appDescription?: string; 20 | vendor: string; 21 | version: string; 22 | folderPath: string; 23 | icon: string; 24 | } 25 | 26 | export declare interface PackageInfo extends RawPackageInfo { 27 | iconUri?: string; 28 | } 29 | 30 | export declare interface StorageInfo { 31 | total: number; 32 | used: number; 33 | available: number; 34 | } 35 | -------------------------------------------------------------------------------- /src/app/types/device.ts: -------------------------------------------------------------------------------- 1 | export declare interface Device { 2 | name: string; 3 | host: string; 4 | port: number; 5 | username: 'prisoner' | 'root' | string; 6 | profile: 'ose'; 7 | privateKey?: { openSsh: string }; 8 | passphrase?: string; 9 | password?: string; 10 | description?: string; 11 | default?: boolean; 12 | indelible?: boolean; 13 | files?: string; 14 | } 15 | 16 | export enum NewDeviceAuthentication { 17 | Password = 'password', 18 | LocalKey = 'localKey', 19 | AppKey = 'appKey', 20 | DevKey = 'devKey', 21 | } 22 | 23 | export const AppPrivKeyName = 'id_devman'; 24 | 25 | export declare interface NewDeviceBase extends Omit { 26 | new: true; 27 | name: string; 28 | description?: string; 29 | host: string; 30 | port: number; 31 | username: string; 32 | } 33 | 34 | export declare interface NewDeviceWithPassword extends NewDeviceBase { 35 | password: string; 36 | } 37 | 38 | export declare interface NewDeviceWithLocalPrivateKey extends NewDeviceBase { 39 | privateKey: { 40 | openSsh: string; 41 | }; 42 | passphrase?: string; 43 | } 44 | 45 | export declare interface NewDeviceWithAppPrivateKey extends NewDeviceBase { 46 | privateKey: { 47 | openSsh: typeof AppPrivKeyName; 48 | }; 49 | passphrase?: string; 50 | } 51 | 52 | export declare interface NewDeviceWithDevicePrivateKey extends NewDeviceBase { 53 | privateKey: { 54 | openSshData: string; 55 | }; 56 | passphrase: string; 57 | } 58 | 59 | export type NewDevice = 60 | NewDeviceWithPassword 61 | | NewDeviceWithLocalPrivateKey 62 | | NewDeviceWithAppPrivateKey 63 | | NewDeviceWithDevicePrivateKey; 64 | 65 | export type DeviceLike = Device | NewDevice; 66 | -------------------------------------------------------------------------------- /src/app/types/file-session.ts: -------------------------------------------------------------------------------- 1 | import {ProgressCallback} from "../core/services/progress-callback"; 2 | 3 | export type FileType = '-' | 'd' | 'c' | 'b' | 's' | 'p' | 'l' | ''; 4 | 5 | export declare interface FileItem { 6 | filename: string; 7 | type: FileType; 8 | mode: string; 9 | user?: string; 10 | group?: string; 11 | size: number, 12 | mtime: number, 13 | link?: LinkInfo; 14 | access?: PermInfo; 15 | } 16 | 17 | export declare interface LinkInfo { 18 | target?: string; 19 | broken?: boolean; 20 | } 21 | 22 | export declare interface PermInfo { 23 | read: boolean; 24 | write: boolean; 25 | execute: boolean; 26 | } 27 | 28 | export declare interface FileSession { 29 | 30 | ls(path: string): Promise; 31 | 32 | rm(path: string, recursive: boolean): Promise; 33 | 34 | get(remotePath: string, localPath: string): Promise; 35 | 36 | put(localPath: string, remotePath: string): Promise; 37 | 38 | mkdir(path: string): Promise; 39 | 40 | getTemp(remotePath: string, progress?: ProgressCallback): Promise; 41 | 42 | uploadBatch(sources: string[], pwd: string, fileCb: (name: string, index: number, total: number) => void, 43 | progressCb: ProgressCallback, failCb: (name: string, e: Error) => Promise): Promise; 44 | 45 | home(): Promise; 46 | } 47 | -------------------------------------------------------------------------------- /src/app/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './device-manager'; 2 | export * from './file-session'; 3 | export * from './device'; 4 | -------------------------------------------------------------------------------- /src/app/types/luna-apis.ts: -------------------------------------------------------------------------------- 1 | import {LunaResponse} from "../core/services/remote-luna.service"; 2 | 3 | export declare interface SystemInfo extends LunaResponse { 4 | firmwareVersion: string; 5 | modelName: string; 6 | sdkVersion: string; 7 | otaId: string; 8 | } 9 | 10 | export declare interface OsInfo extends LunaResponse { 11 | device_name: string; 12 | webos_manufacturing_version: string; 13 | webos_release: string; 14 | } 15 | 16 | export declare interface HomebrewChannelConfiguration extends LunaResponse { 17 | root: boolean, 18 | telnetDisabled: boolean, 19 | failsafe: boolean, 20 | sshdEnabled: boolean, 21 | blockUpdates: boolean 22 | } 23 | -------------------------------------------------------------------------------- /src/app/update-details/update-details.component.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /src/app/update-details/update-details.component.scss: -------------------------------------------------------------------------------- 1 | .update-note { 2 | 3 | img { 4 | max-width: 100%; 5 | } 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/app/update-details/update-details.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject, SecurityContext, ViewEncapsulation} from '@angular/core'; 2 | import {DomSanitizer} from '@angular/platform-browser'; 3 | import * as marked from 'marked'; 4 | import {Release} from '../core/services'; 5 | 6 | @Component({ 7 | selector: 'app-update-details', 8 | templateUrl: './update-details.component.html', 9 | styleUrls: ['./update-details.component.scss'], 10 | encapsulation: ViewEncapsulation.None, 11 | }) 12 | export class UpdateDetailsComponent { 13 | 14 | public bodyHtml: string; 15 | 16 | constructor(@Inject('release') public release: Release, sanitizer: DomSanitizer) { 17 | this.bodyHtml = sanitizer.sanitize(SecurityContext.HTML, marked.marked(release.body)) || 'No description.'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/icons/electron.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/assets/icons/electron.bmp -------------------------------------------------------------------------------- /src/assets/icons/favicon.256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/assets/icons/favicon.256x256.png -------------------------------------------------------------------------------- /src/assets/icons/favicon.512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/assets/icons/favicon.512x512.png -------------------------------------------------------------------------------- /src/assets/icons/favicon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/assets/icons/favicon.icns -------------------------------------------------------------------------------- /src/assets/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/assets/icons/favicon.ico -------------------------------------------------------------------------------- /src/assets/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/assets/icons/favicon.png -------------------------------------------------------------------------------- /src/assets/images/hint-devmode-passphrase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/assets/images/hint-devmode-passphrase.png -------------------------------------------------------------------------------- /src/assets/images/hint-key-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/assets/images/hint-key-server.png -------------------------------------------------------------------------------- /src/environments/environment.dev.ts: -------------------------------------------------------------------------------- 1 | export const AppConfig = { 2 | production: false, 3 | environment: 'development' 4 | }; 5 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const AppConfig = { 2 | production: true, 3 | environment: 'production' 4 | }; 5 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const AppConfig = { 2 | production: false, 3 | environment: 'local' 4 | }; 5 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Device Manager for webOS 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import {enableProdMode} from '@angular/core'; 2 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; 3 | 4 | import {AppModule} from './app/app.module'; 5 | import {AppConfig} from './environments/environment'; 6 | import ReleaseInfo from './release.json'; 7 | import {browserTracingIntegration, defaultStackParser, init as initSentry} from "@sentry/angular"; 8 | 9 | initSentry({ 10 | dsn: "https://93c623f5a47940f0b7bac7d0d5f6a91f@o4504977150377984.ingest.sentry.io/4504978685689856", 11 | tracePropagationTargets: [], 12 | integrations: [ 13 | browserTracingIntegration() 14 | ], 15 | enabled: !!ReleaseInfo.version, 16 | environment: AppConfig.environment, 17 | release: ReleaseInfo.version || 'local', 18 | stackParser: (stack: string, skipFirst?: number) => { 19 | // noinspection HttpUrlsUsage 20 | stack = stack.replace(/@tauri:\/\//g, "@http://"); 21 | return defaultStackParser(stack, skipFirst); 22 | }, 23 | beforeBreadcrumb: (breadcrumb) => { 24 | return breadcrumb.level !== 'debug' ? breadcrumb : null; 25 | }, 26 | beforeSendTransaction: () => { 27 | return null; 28 | }, 29 | beforeSend: (event, hint) => { 30 | const originalException: any = hint.originalException; 31 | if (originalException && originalException['reason']) { 32 | if (originalException['unhandled'] !== true) { 33 | return null; 34 | } 35 | } 36 | return event; 37 | }, 38 | // Set tracesSampleRate to 1.0 to capture 100% 39 | // of transactions for performance monitoring. 40 | // We recommend adjusting this value in production 41 | tracesSampleRate: 1.0, 42 | }); 43 | 44 | if (AppConfig.production) { 45 | enableProdMode(); 46 | } 47 | 48 | platformBrowserDynamic() 49 | .bootstrapModule(AppModule, { 50 | preserveWhitespaces: false 51 | }) 52 | .catch(err => console.error(err)); 53 | 54 | const darkTheme = window.matchMedia('(prefers-color-scheme: dark)'); 55 | 56 | document.documentElement.setAttribute('data-bs-theme', darkTheme.matches ? 'dark' : 'light'); 57 | if (darkTheme.addEventListener) { 58 | darkTheme.addEventListener('change', (media) => { 59 | document.documentElement.setAttribute('data-bs-theme', media.matches ? 'dark' : 'light'); 60 | }); 61 | } else { 62 | // noinspection JSDeprecatedSymbols 63 | darkTheme.addListener?.((ev) => document.documentElement.setAttribute( 64 | 'data-bs-theme', ev.matches ? 'dark' : 'light')); 65 | } 66 | -------------------------------------------------------------------------------- /src/polyfills-test.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/es/reflect'; 2 | import 'zone.js'; 3 | -------------------------------------------------------------------------------- /src/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/repo": { 3 | "target": "http://localhost:8010/", 4 | "secure": false, 5 | "changeOrigin": true, 6 | "pathRewrite": { 7 | "^/repo": "" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/release.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "" 3 | } 4 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @use 'bootstrap' as bs; 2 | @use 'bootstrap-icons'; 3 | @use '@xterm/xterm'; 4 | 5 | @import './styles/no-select'; 6 | @import './styles/app-item'; 7 | @import './styles/overflow'; 8 | @import './styles/terminal'; 9 | @import './styles/shared'; 10 | 11 | 12 | @import 'highlight.js/scss/github.scss' screen and (prefers-color-scheme: light); 13 | @import 'highlight.js/scss/github-dark.scss' screen and (prefers-color-scheme: dark); 14 | 15 | /* 16 | * Content 17 | */ 18 | 19 | html, body { 20 | height: 100%; 21 | width: 100%; 22 | overflow: hidden; 23 | } 24 | 25 | .bg-panel { 26 | @extend .bg-dark-subtle; 27 | } 28 | -------------------------------------------------------------------------------- /src/styles/app-item.scss: -------------------------------------------------------------------------------- 1 | li.app-item { 2 | 3 | .app-desc { 4 | display: flex; 5 | flex-flow: row; 6 | min-height: 60px; 7 | vertical-align: middle; 8 | 9 | img.app-icon { 10 | width: 48px; 11 | height: 48px; 12 | margin-top: auto; 13 | margin-bottom: auto; 14 | } 15 | 16 | .app-headline { 17 | margin-top: auto; 18 | margin-bottom: auto; 19 | 20 | .app-title { 21 | font-weight: bold; 22 | font-size: larger; 23 | } 24 | 25 | .app-description { 26 | } 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/styles/no-select.scss: -------------------------------------------------------------------------------- 1 | /* disable selection */ 2 | :not(input):not(textarea), 3 | :not(input):not(textarea)::after, 4 | :not(input):not(textarea)::before { 5 | user-select: none; 6 | cursor: default; 7 | } 8 | 9 | input, button, textarea, :focus { 10 | outline: none; 11 | } 12 | 13 | button, a { 14 | cursor: pointer !important; 15 | 16 | ::before { 17 | cursor: pointer !important; 18 | } 19 | 20 | * { 21 | cursor: pointer !important; 22 | } 23 | } 24 | 25 | /* disable image and anchor dragging */ 26 | a:not([draggable=true]), img:not([draggable=true]) { 27 | user-drag: none; 28 | } 29 | 30 | a[href^="http://"], 31 | a[href^="https://"], 32 | a[href^="ftp://"] { 33 | user-drag: auto; 34 | } 35 | 36 | .user-select-text * { 37 | user-select: text !important; 38 | cursor: text !important; 39 | } 40 | -------------------------------------------------------------------------------- /src/styles/overflow.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webosbrew/dev-manager-desktop/0a50478206abd072cede0029e155ef2b97e6c9c9/src/styles/overflow.scss -------------------------------------------------------------------------------- /src/styles/shared.scss: -------------------------------------------------------------------------------- 1 | .manager-toolbar { 2 | height: 60px; 3 | flex: 0 0 auto; 4 | } 5 | 6 | .ng-touched.ng-invalid { 7 | @extend .is-invalid; 8 | } 9 | 10 | 11 | .stat-bar { 12 | height: 30px; 13 | } 14 | 15 | .storage-info-bar { 16 | width: 25%; 17 | max-width: 110px; 18 | } 19 | 20 | i.bi.larger::before { 21 | display: block; 22 | font-size: 1.5em; 23 | margin: 0 -0.5em; 24 | } 25 | -------------------------------------------------------------------------------- /src/styles/terminal.scss: -------------------------------------------------------------------------------- 1 | 2 | .terminal-container { 3 | position: absolute; 4 | top: 0; 5 | bottom: 0; 6 | left: 0; 7 | right: 0; 8 | 9 | .xterm { 10 | height: 100% !important; 11 | padding: 5px; 12 | 13 | .xterm-viewport { 14 | //width: 100% !important; 15 | height: 100% !important; 16 | 17 | overflow-y: scroll; 18 | overflow-x: hidden; 19 | } 20 | } 21 | 22 | } 23 | 24 | .terminal-search-bar { 25 | right: 0; 26 | } 27 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare const nodeModule: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | interface Window { 7 | process: any; 8 | require: any; 9 | } 10 | 11 | declare module '*.sh' { 12 | const content: string; 13 | export default content; 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "./out-tsc/app", 7 | "types": [] 8 | }, 9 | "files": [ 10 | "src/main.ts", 11 | "src/polyfills.ts", 12 | ], 13 | "include": [ 14 | "src/**/*.d.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "compileOnSave": false, 5 | "compilerOptions": { 6 | "outDir": "./dist/out-tsc", 7 | "strict": true, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "skipLibCheck": true, 13 | "isolatedModules": true, 14 | "esModuleInterop": true, 15 | "sourceMap": true, 16 | "declaration": false, 17 | "experimentalDecorators": true, 18 | "moduleResolution": "bundler", 19 | "importHelpers": true, 20 | "target": "ES2022", 21 | "module": "ES2022", 22 | "lib": [ 23 | "ES2022", 24 | "dom" 25 | ] 26 | }, 27 | "angularCompilerOptions": { 28 | "enableI18nLegacyMessageIdFormat": false, 29 | "strictInjectionParameters": true, 30 | "strictInputAccessModifiers": true, 31 | "strictTemplates": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "./out-tsc/spec", 7 | "types": [ 8 | "jasmine" 9 | ] 10 | }, 11 | "include": [ 12 | "src/**/*.spec.ts", 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | --------------------------------------------------------------------------------