diff --git a/.all-contributorsrc b/.all-contributorsrc index 021fcdad..63be7acf 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1319,6 +1319,15 @@ "contributions": [ "code" ] + }, + { + "login": "marko1616", + "name": "marko1616", + "avatar_url": "https://avatars.githubusercontent.com/u/45327989?v=4", + "profile": "https://github.com/marko1616", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a5f94f25..3c9fbfd0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -243,13 +243,15 @@ jobs: SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} - name: Upload packages to packagecloud.io - uses: Eugeny/packagecloud-action@main + uses: TykTechnologies/packagecloud-action@main if: github.repository == 'Eugeny/tabby' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') env: PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }} with: repo: 'eugeny/tabby' dir: 'dist' + rpmvers: 'el/9 el/8 ol/6 ol/7' + debvers: 'ubuntu/bionic ubuntu/focal ubuntu/hirsute ubuntu/impish ubuntu/jammy ubuntu/kinetic ubuntu/noble debian/jessie debian/stretch debian/buster' - uses: actions/upload-artifact@master name: Upload AppImage (${{matrix.arch}}) diff --git a/README.de-DE.md b/README.de-DE.md index cb222f0a..457fbed6 100644 --- a/README.de-DE.md +++ b/README.de-DE.md @@ -341,6 +341,7 @@ Dank geht an diese wunderbaren Menschen ([emoji key](https://allcontributors.org Charles Buffington
Charles Buffington

πŸ’» Yu Qin
Yu Qin

πŸ’» fireblue
fireblue

πŸ’» + marko1616
marko1616

πŸ’» diff --git a/README.es-ES.md b/README.es-ES.md index 416cab43..45b0a7ba 100644 --- a/README.es-ES.md +++ b/README.es-ES.md @@ -343,6 +343,7 @@ Gracias a estas maravillosas personas ([emoji key](https://allcontributors.org/d Charles Buffington
Charles Buffington

πŸ’» Yu Qin
Yu Qin

πŸ’» fireblue
fireblue

πŸ’» + marko1616
marko1616

πŸ’» diff --git a/README.id-ID.md b/README.id-ID.md index b33e1980..07604021 100644 --- a/README.id-ID.md +++ b/README.id-ID.md @@ -340,6 +340,7 @@ Terima kasih kepada mereka yang telah membantu ([emoji key](https://allcontribut Charles Buffington
Charles Buffington

πŸ’» Yu Qin
Yu Qin

πŸ’» fireblue
fireblue

πŸ’» + marko1616
marko1616

πŸ’» diff --git a/README.it-IT.md b/README.it-IT.md index 065a5340..c527aaba 100644 --- a/README.it-IT.md +++ b/README.it-IT.md @@ -336,6 +336,7 @@ Grazie a queste persone meravigliose ([emoji key](https://allcontributors.org/do Charles Buffington
Charles Buffington

πŸ’» Yu Qin
Yu Qin

πŸ’» fireblue
fireblue

πŸ’» + marko1616
marko1616

πŸ’» diff --git a/README.ja-JP.md b/README.ja-JP.md index 8c161d60..2d8330d4 100644 --- a/README.ja-JP.md +++ b/README.ja-JP.md @@ -351,6 +351,7 @@ WindowsδΈŠγ§γ―γ€`Tabby.exe`γŒγ‚γ‚‹ε ΄ζ‰€γ¨εŒγ˜ε ΄ζ‰€γ«`data`フォル Charles Buffington
Charles Buffington

πŸ’» Yu Qin
Yu Qin

πŸ’» fireblue
fireblue

πŸ’» + marko1616
marko1616

πŸ’» diff --git a/README.ko-KR.md b/README.ko-KR.md index f56bae66..9a8ed25b 100644 --- a/README.ko-KR.md +++ b/README.ko-KR.md @@ -335,6 +335,7 @@ Pull requests and plugins are welcome! Charles Buffington
Charles Buffington

πŸ’» Yu Qin
Yu Qin

πŸ’» fireblue
fireblue

πŸ’» + marko1616
marko1616

πŸ’» diff --git a/README.md b/README.md index 7a34d9ea..e714a33c 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,6 @@ > πŸ‘‹ Managing remote environments? Check out [Warpgate, my smart SSH/HTTP/MySQL bastion server](https://github.com/warp-tech/warpgate), it works great with Tabby, you'll love it. -> πŸ‘‹ [Tabby-web](https://github.com/Eugeny/tabby-web) is looking for sponsors. As I can't afford to host it myself any longer, I'm looking for a sponsor to cover the hosting costs. If you're interested, please [get in touch](https://twitter.com/eugeeeeny)! - - ---- ### Downloads: @@ -155,6 +152,11 @@ Plugins and themes can be installed directly from the Settings view inside Tabby [**keygen**](https://keygen.sh/?via=eugene) has provided free release & auto-update hosting + + +[**IQ Hive**](https://iqhive.com) is providing financial support for the project development + + # Contributing @@ -357,6 +359,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Charles Buffington
Charles Buffington

πŸ’» Yu Qin
Yu Qin

πŸ’» fireblue
fireblue

πŸ’» + marko1616
marko1616

πŸ’» diff --git a/README.pt-BR.md b/README.pt-BR.md index e920e49d..703b0eaf 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -344,6 +344,7 @@ Obrigado vai para essas pessoas maravilhosas ([emoji key](https://allcontributor Charles Buffington
Charles Buffington

πŸ’» Yu Qin
Yu Qin

πŸ’» fireblue
fireblue

πŸ’» + marko1616
marko1616

πŸ’» diff --git a/README.ru-RU.md b/README.ru-RU.md index bf71870d..a96a3505 100644 --- a/README.ru-RU.md +++ b/README.ru-RU.md @@ -336,6 +336,7 @@ Pull-запросы ΠΈ ΠΏΠ»Π°Π³ΠΈΠ½Ρ‹ ΠΏΡ€ΠΈΠ²Π΅Ρ‚ΡΡ‚Π²ΡƒΡŽΡ‚ΡΡ! Charles Buffington
Charles Buffington

πŸ’» Yu Qin
Yu Qin

πŸ’» fireblue
fireblue

πŸ’» + marko1616
marko1616

πŸ’» diff --git a/README.zh-CN.md b/README.zh-CN.md index 162512ad..1e7b693f 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -335,6 +335,7 @@ Charles Buffington
Charles Buffington

πŸ’» Yu Qin
Yu Qin

πŸ’» fireblue
fireblue

πŸ’» + marko1616
marko1616

πŸ’» diff --git a/build/mac/entitlements.plist b/build/mac/entitlements.plist index 350aa189..25c9ccd5 100644 --- a/build/mac/entitlements.plist +++ b/build/mac/entitlements.plist @@ -2,6 +2,8 @@ + com.apple.security.automation.apple-events + com.apple.security.cs.allow-jit com.apple.security.cs.allow-unsigned-executable-memory diff --git a/electron-builder.yml b/electron-builder.yml index 8e6262d2..e83658b0 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -51,6 +51,7 @@ mac: entitlements: "./build/mac/entitlements.plist" entitlementsInherit: "./build/mac/entitlements.plist" extendInfo: + ElectronTeamID: ${teamId} NSRequiresAquaSystemAppearance: false NSCameraUsageDescription: "A subprocess requests access to the device's camera." NSMicrophoneUsageDescription: "A subprocess requests access to the device's microphone." @@ -60,6 +61,7 @@ mac: NSDownloadsFolderUsageDescription: "A subprocess requests access to the user's Downloads folder." NSNetworkVolumesUsageDescription: 'A subprocess requests access to files on a network volume.' NSRemovableVolumesUsageDescription: 'A subprocess requests access to files on a removable volume.' + NSAppleEventsUsageDescription: 'A subprocess requests permission to send AppleScript events to another application.' linux: category: "Utility;TerminalEmulator;System" diff --git a/scripts/build-macos.mjs b/scripts/build-macos.mjs index 1084621b..015f60d2 100755 --- a/scripts/build-macos.mjs +++ b/scripts/build-macos.mjs @@ -24,6 +24,7 @@ builder({ config: { extraMetadata: { version: vars.version, + teamId: process.env.APPLE_TEAM_ID, }, mac: { identity: !process.env.CI || process.env.CSC_LINK ? undefined : null, diff --git a/tabby-core/src/api/index.ts b/tabby-core/src/api/index.ts index cc467f96..b55f7e76 100644 --- a/tabby-core/src/api/index.ts +++ b/tabby-core/src/api/index.ts @@ -10,7 +10,7 @@ export { Theme } from './theme' export { TabContextMenuItemProvider } from './tabContextMenuProvider' export { SelectorOption } from './selector' export { CLIHandler, CLIEvent } from './cli' -export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions, FileDownload, FileUpload, FileTransfer, HTMLFileUpload, FileUploadOptions } from './platform' +export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions, FileDownload, FileUpload, FileTransfer, HTMLFileUpload, FileUploadOptions, DirectoryUpload } from './platform' export { MenuItemOptions } from './menu' export { BootstrapData, PluginInfo, BOOTSTRAP_DATA } from './mainProcess' export { HostWindowService } from './hostWindow' diff --git a/tabby-core/src/api/platform.ts b/tabby-core/src/api/platform.ts index df89abe3..2cb33810 100644 --- a/tabby-core/src/api/platform.ts +++ b/tabby-core/src/api/platform.ts @@ -88,6 +88,26 @@ export interface FileUploadOptions { multiple: boolean } +export class DirectoryUpload { + private childrens: (FileUpload|DirectoryUpload)[] = [] + + constructor (private name = '') { + // Just set name for now. + } + + getName (): string { + return this.name + } + + getChildrens (): (FileUpload|DirectoryUpload)[] { + return this.childrens + } + + pushChildren (item: FileUpload|DirectoryUpload): void { + this.childrens.push(item) + } +} + export type PlatformTheme = 'light'|'dark' export abstract class PlatformService { @@ -108,23 +128,54 @@ export abstract class PlatformService { abstract startDownload (name: string, mode: number, size: number): Promise abstract startUpload (options?: FileUploadOptions): Promise + abstract startUploadDirectory (paths?: string[]): Promise + + async startUploadFromDragEvent (event: DragEvent, multiple = false): Promise { + const result = new DirectoryUpload() - startUploadFromDragEvent (event: DragEvent, multiple = false): FileUpload[] { - const result: FileUpload[] = [] if (!event.dataTransfer) { - return [] + return Promise.resolve(result) } + + const traverseFileTree = (item: any, root: DirectoryUpload = result): Promise => { + return new Promise((resolve) => { + if (item.isFile) { + item.file((file: File) => { + const transfer = new HTMLFileUpload(file) + this.fileTransferStarted.next(transfer) + root.pushChildren(transfer) + resolve() + }) + } else if (item.isDirectory) { + const dirReader = item.createReader() + const childrenFolder = new DirectoryUpload(item.name) + dirReader.readEntries(async (entries: any[]) => { + for (const entry of entries) { + await traverseFileTree(entry, childrenFolder) + } + resolve() + }) + root.pushChildren(childrenFolder) + } else { + resolve() + } + }) + } + + const promises: Promise[] = [] + + const items = event.dataTransfer.items // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < event.dataTransfer.files.length; i++) { - const file = event.dataTransfer.files[i] - const transfer = new HTMLFileUpload(file) - this.fileTransferStarted.next(transfer) - result.push(transfer) - if (!multiple) { - break + for (let i = 0; i < items.length; i++) { + const item = items[i].webkitGetAsEntry() + if (item) { + promises.push(traverseFileTree(item)) + if (!multiple) { + break + } } } - return result + return Promise.all(promises).then(() => result) } getConfigPath (): string|null { diff --git a/tabby-core/src/directives/dropZone.directive.ts b/tabby-core/src/directives/dropZone.directive.ts index 1da823eb..64632fbc 100644 --- a/tabby-core/src/directives/dropZone.directive.ts +++ b/tabby-core/src/directives/dropZone.directive.ts @@ -1,5 +1,5 @@ import { Directive, Output, ElementRef, EventEmitter, AfterViewInit } from '@angular/core' -import { FileUpload, PlatformService } from '../api/platform' +import { DirectoryUpload, PlatformService } from '../api/platform' import './dropZone.directive.scss' /** @hidden */ @@ -7,7 +7,7 @@ import './dropZone.directive.scss' selector: '[dropZone]', }) export class DropZoneDirective implements AfterViewInit { - @Output() transfer = new EventEmitter() + @Output() transfer = new EventEmitter() private dropHint?: HTMLElement constructor ( @@ -27,11 +27,9 @@ export class DropZoneDirective implements AfterViewInit { }) } }) - this.el.nativeElement.addEventListener('drop', (event: DragEvent) => { + this.el.nativeElement.addEventListener('drop', async (event: DragEvent) => { this.removeHint() - for (const transfer of this.platform.startUploadFromDragEvent(event, true)) { - this.transfer.emit(transfer) - } + this.transfer.emit(await this.platform.startUploadFromDragEvent(event, true)) }) this.el.nativeElement.addEventListener('dragleave', () => { this.removeHint() diff --git a/tabby-core/src/services/homeBase.service.ts b/tabby-core/src/services/homeBase.service.ts index d64d5217..b508aa88 100644 --- a/tabby-core/src/services/homeBase.service.ts +++ b/tabby-core/src/services/homeBase.service.ts @@ -27,12 +27,8 @@ export class HomeBaseService { this.platform.openExternal('https://github.com/Eugeny/tabby') } - openDiscussions (): void { - this.platform.openExternal('https://github.com/Eugeny/tabby/discussions') - } - - openTwitter (): void { - this.platform.openExternal('https://twitter.com/eugeeeeny') + openDiscord (): void { + this.platform.openExternal('https://discord.gg/4c5EVTBhtp') } openTranslations (): void { diff --git a/tabby-electron/src/services/platform.service.ts b/tabby-electron/src/services/platform.service.ts index 9db5aa0b..d08f07ef 100644 --- a/tabby-electron/src/services/platform.service.ts +++ b/tabby-electron/src/services/platform.service.ts @@ -5,7 +5,7 @@ import * as os from 'os' import promiseIpc, { RendererProcessType } from 'electron-promise-ipc' import { execFile } from 'mz/child_process' import { Injectable, NgZone } from '@angular/core' -import { PlatformService, ClipboardContent, Platform, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload, FileDownload, FileUploadOptions, wrapPromise, TranslateService } from 'tabby-core' +import { PlatformService, ClipboardContent, Platform, MenuItemOptions, MessageBoxOptions, MessageBoxResult, DirectoryUpload, FileUpload, FileDownload, FileUploadOptions, wrapPromise, TranslateService } from 'tabby-core' import { ElectronService } from '../services/electron.service' import { ElectronHostWindow } from './hostWindow.service' import { ShellIntegrationService } from './shellIntegration.service' @@ -48,6 +48,21 @@ export class ElectronPlatformService extends PlatformService { }) } + async getAllFiles (dir: string, root: DirectoryUpload): Promise { + const items = await fs.readdir(dir, { withFileTypes: true }) + for (const item of items) { + if (item.isDirectory()) { + root.pushChildren(await this.getAllFiles(path.join(dir, item.name), new DirectoryUpload(item.name))) + } else { + const file = new ElectronFileUpload(path.join(dir, item.name), this.electron) + root.pushChildren(file) + await wrapPromise(this.zone, file.open()) + this.fileTransferStarted.next(file) + } + } + return root + } + readClipboard (): string { return this.electron.clipboard.readText() } @@ -216,6 +231,28 @@ export class ElectronPlatformService extends PlatformService { })) } + async startUploadDirectory (paths?: string[]): Promise { + const properties: any[] = ['openFile', 'treatPackageAsDirectory', 'openDirectory'] + + if (!paths) { + const result = await this.electron.dialog.showOpenDialog( + this.hostWindow.getWindow(), + { + buttonLabel: this.translate.instant('Select'), + properties, + }, + ) + if (result.canceled) { + return new DirectoryUpload() + } + paths = result.filePaths + } + + const root = new DirectoryUpload() + root.pushChildren(await this.getAllFiles(paths[0].split(path.sep).join(path.posix.sep), new DirectoryUpload(path.basename(paths[0])))) + return root + } + async startDownload (name: string, mode: number, size: number, filePath?: string): Promise { if (!filePath) { const result = await this.electron.dialog.showSaveDialog( diff --git a/tabby-settings/src/components/settingsTab.component.pug b/tabby-settings/src/components/settingsTab.component.pug index e923d29f..1b2e8e95 100644 --- a/tabby-settings/src/components/settingsTab.component.pug +++ b/tabby-settings/src/components/settingsTab.component.pug @@ -40,11 +40,11 @@ div(translate) Report a problem small.text-muted(translate) Generate a pre-filled GitHub issue - button.list-group-item.list-group-item-action.link-card((click)='homeBase.openDiscussions()') - i.fas.fa-fw.fa-comments + button.list-group-item.list-group-item-action.link-card((click)='homeBase.openDiscord()') + i.fab.fa-fw.fa-discord div - div(translate) Ask a question - small.text-muted(translate) On GitHub Discussions + div(translate) Community + small.text-muted(translate) On Discord button.list-group-item.list-group-item-action.link-card((click)='homeBase.openGitHub()') i.fab.fa-fw.fa-github @@ -58,12 +58,6 @@ div(translate) What's new small.text-muted(translate) Show release notes - button.list-group-item.list-group-item-action.link-card((click)='homeBase.openTwitter()') - i.fab.fa-fw.fa-twitter - div - div(translate) Subscribe to updates - small.text-muted(translate) Tabby news and updates on Twitter - h3(translate) Application settings .form-line diff --git a/tabby-ssh/src/components/sftpPanel.component.pug b/tabby-ssh/src/components/sftpPanel.component.pug index 57e96941..67e4f202 100644 --- a/tabby-ssh/src/components/sftpPanel.component.pug +++ b/tabby-ssh/src/components/sftpPanel.component.pug @@ -23,11 +23,15 @@ button.btn.btn-link.btn-sm.flex-shrink-0.d-flex((click)='upload()') i.fas.fa-upload.me-1 - div(translate) Upload + div(translate) Upload files + + button.btn.btn-link.btn-sm.flex-shrink-0.d-flex((click)='uploadFolder()') + i.fas.fa-upload.me-1 + div(translate) Upload folder button.btn.btn-link.text-decoration-none((click)='close()') !{require('../../../tabby-core/src/icons/times.svg')} -.body(dropZone, (transfer)='uploadOne($event)') +.body(dropZone, (transfer)='uploadOneFolder($event)') a.alert.alert-info.d-flex.align-items-center( *ngIf='shouldShowCWDTip && !cwdDetectionAvailable', (click)='platform.openExternal("https://tabby.sh/go/cwd-detection")' diff --git a/tabby-ssh/src/components/sftpPanel.component.ts b/tabby-ssh/src/components/sftpPanel.component.ts index e9662443..38625c0f 100644 --- a/tabby-ssh/src/components/sftpPanel.component.ts +++ b/tabby-ssh/src/components/sftpPanel.component.ts @@ -1,7 +1,7 @@ import * as C from 'constants' import { posix as path } from 'path' import { Component, Input, Output, EventEmitter, Inject, Optional } from '@angular/core' -import { FileUpload, MenuItemOptions, NotificationsService, PlatformService } from 'tabby-core' +import { FileUpload, DirectoryUpload, MenuItemOptions, NotificationsService, PlatformService } from 'tabby-core' import { SFTPSession, SFTPFile } from '../session/sftp' import { SSHSession } from '../session/ssh' import { SFTPContextMenuItemProvider } from '../api' @@ -180,6 +180,30 @@ export class SFTPPanelComponent { await Promise.all(transfers.map(t => this.uploadOne(t))) } + async uploadFolder (): Promise { + const transfer = await this.platform.startUploadDirectory() + await this.uploadOneFolder(transfer) + } + + async uploadOneFolder (transfer: DirectoryUpload, accumPath = ''): Promise { + const savedPath = this.path + for(const t of transfer.getChildrens()) { + if (t instanceof DirectoryUpload) { + try { + await this.sftp.mkdir(path.posix.join(this.path, accumPath, t.getName())) + } catch { + // Intentionally ignoring errors from making duplicate dirs. + } + await this.uploadOneFolder(t, path.posix.join(accumPath, t.getName())) + } else { + await this.sftp.upload(path.posix.join(this.path, accumPath, t.getName()), t) + } + } + if (this.path === savedPath) { + await this.navigate(this.path) + } + } + async uploadOne (transfer: FileUpload): Promise { const savedPath = this.path await this.sftp.upload(path.join(this.path, transfer.getName()), transfer) diff --git a/tabby-web/src/platform.ts b/tabby-web/src/platform.ts index d64b50f4..54a7c6db 100644 --- a/tabby-web/src/platform.ts +++ b/tabby-web/src/platform.ts @@ -2,7 +2,7 @@ import '@vaadin/vaadin-context-menu' import copyToClipboard from 'copy-text-to-clipboard' import { Injectable, Inject } from '@angular/core' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' -import { PlatformService, ClipboardContent, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload, FileUploadOptions, FileDownload, HTMLFileUpload } from 'tabby-core' +import { PlatformService, ClipboardContent, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload, FileUploadOptions, FileDownload, HTMLFileUpload, DirectoryUpload } from 'tabby-core' // eslint-disable-next-line no-duplicate-imports import type { ContextMenuElement, ContextMenuItem } from '@vaadin/vaadin-context-menu' @@ -135,6 +135,10 @@ export class WebPlatformService extends PlatformService { }) } + async startUploadDirectory (_paths?: string[]): Promise { + return new DirectoryUpload() + } + setErrorHandler (handler: (_: any) => void): void { window.addEventListener('error', handler) }