@electerm/electerm-react 3.2.0 → 3.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/client/common/constants.js +1 -8
  2. package/client/common/fs.js +84 -0
  3. package/client/components/batch-op/batch-op-alert.jsx +23 -0
  4. package/client/components/batch-op/batch-op-editor.jsx +206 -0
  5. package/client/components/batch-op/batch-op-logs.jsx +53 -0
  6. package/client/components/batch-op/batch-op-runner.jsx +315 -0
  7. package/client/components/bookmark-form/ai-bookmark-form.jsx +2 -1
  8. package/client/components/bookmark-form/bookmark-from-history-modal.jsx +2 -1
  9. package/client/components/bookmark-form/common/fields.jsx +15 -0
  10. package/client/components/bookmark-form/config/rdp.js +5 -0
  11. package/client/components/common/auto-check-update.jsx +31 -0
  12. package/client/components/common/notification.styl +1 -1
  13. package/client/components/file-transfer/conflict-resolve.jsx +3 -0
  14. package/client/components/footer/batch-input.jsx +10 -7
  15. package/client/components/main/error-wrapper.jsx +18 -7
  16. package/client/components/main/main.jsx +6 -7
  17. package/client/components/main/upgrade.jsx +133 -104
  18. package/client/components/main/upgrade.styl +2 -2
  19. package/client/components/rdp/file-transfer.js +375 -0
  20. package/client/components/rdp/rdp-session.jsx +169 -76
  21. package/client/components/rdp/rdp.styl +27 -0
  22. package/client/components/setting-sync/auto-sync.jsx +53 -0
  23. package/client/components/setting-sync/data-import.jsx +69 -8
  24. package/client/components/sftp/address-bar.jsx +23 -3
  25. package/client/components/sidebar/bookmark-select.jsx +3 -2
  26. package/client/components/sidebar/history-item.jsx +3 -1
  27. package/client/components/sidebar/index.jsx +0 -9
  28. package/client/components/sidebar/info-modal.jsx +7 -2
  29. package/client/components/tabs/add-btn-menu.jsx +1 -1
  30. package/client/components/tabs/add-btn.jsx +9 -15
  31. package/client/components/tabs/quick-connect.jsx +6 -10
  32. package/client/components/terminal/terminal.jsx +4 -5
  33. package/client/components/tree-list/tree-list.jsx +115 -10
  34. package/client/components/tree-list/tree-list.styl +3 -0
  35. package/client/components/tree-list/tree-search.jsx +9 -1
  36. package/client/components/widgets/widget-form.jsx +6 -0
  37. package/client/store/app-upgrade.js +2 -2
  38. package/client/store/common.js +0 -28
  39. package/client/store/load-data.js +3 -3
  40. package/client/store/mcp-handler.js +2 -2
  41. package/client/store/sync.js +25 -1
  42. package/client/store/tab.js +1 -1
  43. package/client/store/watch.js +10 -18
  44. package/client/views/index.pug +1 -2
  45. package/package.json +1 -1
  46. package/client/components/batch-op/batch-op.jsx +0 -694
@@ -1,3 +1,4 @@
1
+ import React, { useRef, useEffect } from 'react'
1
2
  import {
2
3
  ArrowUpOutlined,
3
4
  EyeInvisibleFilled,
@@ -74,9 +75,15 @@ function renderAddonBefore (props, realPath) {
74
75
  }
75
76
 
76
77
  function renderAddonAfter (isLoadingRemote, onGoto, GoIcon, type) {
78
+ const handleClick = (e) => {
79
+ e.stopPropagation()
80
+ if (!isLoadingRemote) {
81
+ onGoto(type)
82
+ }
83
+ }
77
84
  return (
78
85
  <GoIcon
79
- onClick={isLoadingRemote ? () => null : () => onGoto(type)}
86
+ onClick={handleClick}
80
87
  />
81
88
  )
82
89
  }
@@ -128,15 +135,28 @@ export default function AddressBar (props) {
128
135
  const GoIcon = isLoadingRemote
129
136
  ? LoadingOutlined
130
137
  : (realPath === path ? ReloadOutlined : ArrowRightOutlined)
138
+ const inputRef = useRef(null)
139
+
140
+ useEffect(() => {
141
+ const wrapEl = inputRef.current
142
+ if (!wrapEl) return
143
+ const inputEl = wrapEl.querySelector('input')
144
+ if (!inputEl) return
145
+ const handler = () => props.onInputFocus(type)
146
+ inputEl.addEventListener('click', handler)
147
+ return () => {
148
+ inputEl.removeEventListener('click', handler)
149
+ }
150
+ }, [type])
151
+
131
152
  return (
132
153
  <div className='pd1y sftp-title-wrap'>
133
- <div className='sftp-title'>
154
+ <div className='sftp-title' ref={inputRef}>
134
155
  <Input
135
156
  value={path}
136
157
  onChange={e => props.onChange(e, n)}
137
158
  onPressEnter={e => props.onGoto(type, e)}
138
159
  prefix={renderAddonBefore(props, realPath)}
139
- onFocus={() => props.onInputFocus(type)}
140
160
  onBlur={() => props.onInputBlur(type)}
141
161
  disabled={loadingSftp}
142
162
  suffix={
@@ -6,7 +6,7 @@ import { auto } from 'manate/react'
6
6
  import TreeList from '../tree-list/tree-list'
7
7
 
8
8
  export default auto(function BookmarkSelect (props) {
9
- const { store, from } = props
9
+ const { store, from, autoFocus } = props
10
10
  const {
11
11
  listStyle,
12
12
  openedSideBar,
@@ -38,7 +38,8 @@ export default auto(function BookmarkSelect (props) {
38
38
  bookmarkGroups: store.getBookmarkGroupsTotal(),
39
39
  expandedKeys,
40
40
  leftSidebarWidth,
41
- bookmarkGroupTree: store.bookmarkGroupTree
41
+ bookmarkGroupTree: store.bookmarkGroupTree,
42
+ autoFocus
42
43
  }
43
44
  return (
44
45
  <TreeList
@@ -40,7 +40,9 @@ export default function HistoryItem (props) {
40
40
  e.stopPropagation()
41
41
  refsStatic.get('bookmark-from-history-modal')?.show(item.tab)
42
42
  }
43
-
43
+ if (!item.tab) {
44
+ return null
45
+ }
44
46
  const title = createTitleWithTag(item.tab)
45
47
  const tt = createTitle(item.tab)
46
48
  return (
@@ -6,7 +6,6 @@ import {
6
6
  PlusCircleOutlined,
7
7
  SettingOutlined,
8
8
  UpCircleOutlined,
9
- BarsOutlined,
10
9
  AppstoreOutlined,
11
10
  ThunderboltOutlined
12
11
  } from '@ant-design/icons'
@@ -92,7 +91,6 @@ export default function Sidebar (props) {
92
91
  openAbout,
93
92
  openSettingSync,
94
93
  openTerminalThemes,
95
- toggleBatchOp,
96
94
  setLeftSidePanelWidth
97
95
  } = store
98
96
  const {
@@ -102,7 +100,6 @@ export default function Sidebar (props) {
102
100
  shouldUpgrade
103
101
  } = upgradeInfo
104
102
  const showSetting = showModal === modals.setting
105
- const showBatchOp = showModal === modals.batchOps
106
103
  const settingActive = showSetting && settingTab === settingMap.setting && settingItem.id === 'setting-common'
107
104
  const syncActive = showSetting && settingTab === settingMap.setting && settingItem.id === 'setting-sync'
108
105
  const themeActive = showSetting && settingTab === settingMap.terminalThemes
@@ -190,12 +187,6 @@ export default function Sidebar (props) {
190
187
  spin={isSyncingSetting}
191
188
  />
192
189
  </SideIcon>
193
- <SideIcon
194
- title={e('batchOp')}
195
- active={showBatchOp}
196
- >
197
- <BarsOutlined className='iblock font20 control-icon' onClick={toggleBatchOp} />
198
- </SideIcon>
199
190
  <SideIcon
200
191
  title={e('widgets')}
201
192
  active={widgetsActive}
@@ -42,8 +42,10 @@ export default auto(function InfoModal (props) {
42
42
  upgradeInfo
43
43
  } = props
44
44
  const onCheckUpdating = upgradeInfo.checkingRemoteVersion || upgradeInfo.upgrading
45
+ const { noUpdateMessage, noUpdateMessageExpires } = upgradeInfo
46
+ const showMessage = noUpdateMessage && noUpdateMessageExpires && Date.now() < noUpdateMessageExpires
45
47
  return (
46
- <p className='mg1b mg2t'>
48
+ <div className='mg1b mg2t'>
47
49
  <Button
48
50
  type='primary'
49
51
  loading={onCheckUpdating}
@@ -51,7 +53,10 @@ export default auto(function InfoModal (props) {
51
53
  >
52
54
  {e('checkForUpdate')}
53
55
  </Button>
54
- </p>
56
+ {showMessage && (
57
+ <span className='mg1l update-msg'>{noUpdateMessage}</span>
58
+ )}
59
+ </div>
55
60
  )
56
61
  }
57
62
 
@@ -75,7 +75,7 @@ export default function AddBtnMenu ({
75
75
 
76
76
  let listContent
77
77
  if (activeTab === 'bookmarks') {
78
- listContent = <BookmarksList store={window.store} />
78
+ listContent = <BookmarksList store={window.store} autoFocus />
79
79
  } else {
80
80
  listContent = <History store={window.store} />
81
81
  }
@@ -40,15 +40,18 @@ export default class AddBtn extends Component {
40
40
  componentWillUnmount () {
41
41
  if (this.state.open) {
42
42
  document.removeEventListener('click', this.handleDocumentClick)
43
+ document.removeEventListener('keydown', this.handleKeyDown)
43
44
  }
44
45
  // Clean up portal container
45
46
  if (this.portalContainer) {
46
47
  document.body.removeChild(this.portalContainer)
47
48
  this.portalContainer = null
48
49
  }
49
- // Clear focus timeout
50
- if (this.çƒ) {
51
- clearTimeout(this.focusTimeout)
50
+ }
51
+
52
+ handleKeyDown = (e) => {
53
+ if (e.key === 'Escape') {
54
+ this.setState({ open: false })
52
55
  }
53
56
  }
54
57
 
@@ -56,8 +59,10 @@ export default class AddBtn extends Component {
56
59
  // Attach or detach document click listener only when menu open state changes
57
60
  if (this.state.open && !prevState.open) {
58
61
  document.addEventListener('click', this.handleDocumentClick)
62
+ document.addEventListener('keydown', this.handleKeyDown)
59
63
  } else if (!this.state.open && prevState.open) {
60
64
  document.removeEventListener('click', this.handleDocumentClick)
65
+ document.removeEventListener('keydown', this.handleKeyDown)
61
66
  }
62
67
  }
63
68
 
@@ -115,17 +120,6 @@ export default class AddBtn extends Component {
115
120
  )
116
121
  }
117
122
 
118
- focusSearchInput = () => {
119
- // Focus the search input after the menu renders
120
- this.focusTimeout = setTimeout(() => {
121
- const searchInput = this.menuRef.current?.querySelector('.add-menu-list .ant-input')
122
- if (searchInput) {
123
- searchInput.focus()
124
- searchInput.select()
125
- }
126
- }, 500)
127
- }
128
-
129
123
  handleAddBtnClick = () => {
130
124
  if (this.state.open) {
131
125
  this.setState({ open: false })
@@ -172,7 +166,7 @@ export default class AddBtn extends Component {
172
166
  menuPosition,
173
167
  menuTop,
174
168
  menuLeft
175
- }, this.focusSearchInput)
169
+ })
176
170
 
177
171
  window.openTabBatch = this.props.batch
178
172
  }
@@ -1,5 +1,5 @@
1
1
  import { useState, useRef, useEffect } from 'react'
2
- import { Button, Space } from 'antd'
2
+ import { Button, Space, Input } from 'antd'
3
3
  import { ArrowRightOutlined, ThunderboltOutlined } from '@ant-design/icons'
4
4
  import message from '../common/message'
5
5
  import InputAutoFocus from '../common/input-auto-focus'
@@ -29,18 +29,14 @@ export default function QuickConnect ({ batch, inputOnly }) {
29
29
  const [inputValue, setInputValue] = useState('')
30
30
  const inputRef = useRef(null)
31
31
 
32
- useEffect(() => {
33
- if (showInput && inputRef.current) {
34
- inputRef.current.focus()
35
- }
36
- }, [showInput])
37
-
38
- // When inputOnly is true, always show the input
32
+ // When inputOnly is true, always show the input (without auto-focus)
39
33
  useEffect(() => {
40
34
  if (inputOnly) {
41
35
  setShowInput(true)
36
+ } else if (showInput && inputRef.current) {
37
+ inputRef.current.focus()
42
38
  }
43
- }, [inputOnly])
39
+ }, [inputOnly, showInput])
44
40
 
45
41
  const handleToggle = () => {
46
42
  setShowInput(!showInput)
@@ -94,7 +90,7 @@ export default function QuickConnect ({ batch, inputOnly }) {
94
90
  <Button
95
91
  {...iconsProps1}
96
92
  />
97
- <InputAutoFocus {...inputProps} />
93
+ {inputOnly ? <Input {...inputProps} /> : <InputAutoFocus {...inputProps} />}
98
94
  <Button
99
95
  {...iconProps}
100
96
  />
@@ -378,11 +378,10 @@ class Term extends Component {
378
378
  }
379
379
 
380
380
  runQuickCommand = (cmd, inputOnly = false) => {
381
- this.term && this.attachAddon._sendData(
382
- cmd +
383
- (inputOnly ? '' : '\r')
384
- )
385
- this.term.focus()
381
+ if (this.term && this.attachAddon) {
382
+ this.attachAddon._sendData(cmd + (inputOnly ? '' : '\r'))
383
+ this.term.focus()
384
+ }
386
385
  }
387
386
 
388
387
  cd = (p) => {
@@ -2,6 +2,7 @@
2
2
  * tree list for bookmarks
3
3
  */
4
4
 
5
+ import React from 'react'
5
6
  import { Component } from 'manate/react/class-components'
6
7
  import {
7
8
  CheckOutlined,
@@ -44,6 +45,7 @@ export default class ItemListTree extends Component {
44
45
  categoryColor: '',
45
46
  categoryId: ''
46
47
  }
48
+ this.treeRef = React.createRef()
47
49
  }
48
50
 
49
51
  onSubmit = false
@@ -102,9 +104,55 @@ export default class ItemListTree extends Component {
102
104
  }
103
105
 
104
106
  handleChange = keyword => {
105
- this.setState({
106
- keyword
107
+ this.setState({ keyword })
108
+ }
109
+
110
+ handleKeyDown = (e) => {
111
+ const { keyword } = this.state
112
+ if (!keyword) return
113
+
114
+ const treeContainer = this.treeRef.current
115
+ if (!treeContainer) return
116
+
117
+ const allItems = treeContainer.querySelectorAll('.tree-item')
118
+ const matchedItems = Array.from(allItems).filter(item => {
119
+ const isGroup = item.getAttribute('data-is-group') === 'true'
120
+ if (isGroup) return false
121
+ const title = item.querySelector('.tree-item-title')
122
+ if (!title) return false
123
+ const text = title.textContent || ''
124
+ return text.toLowerCase().includes(keyword.toLowerCase())
107
125
  })
126
+
127
+ if (matchedItems.length === 0) return
128
+
129
+ const currentSelected = treeContainer.querySelector('.tree-item.search-selected')
130
+ let currentIndex = -1
131
+ if (currentSelected) {
132
+ currentSelected.classList.remove('search-selected')
133
+ currentIndex = matchedItems.indexOf(currentSelected)
134
+ }
135
+
136
+ if (e.key === 'ArrowDown') {
137
+ e.preventDefault()
138
+ const nextIndex = (currentIndex + 1) % matchedItems.length
139
+ matchedItems[nextIndex].classList.add('search-selected')
140
+ matchedItems[nextIndex].scrollIntoView({ block: 'nearest' })
141
+ } else if (e.key === 'ArrowUp') {
142
+ e.preventDefault()
143
+ const nextIndex = currentIndex <= 0 ? matchedItems.length - 1 : currentIndex - 1
144
+ matchedItems[nextIndex].classList.add('search-selected')
145
+ matchedItems[nextIndex].scrollIntoView({ block: 'nearest' })
146
+ } else if (e.key === 'Enter') {
147
+ e.preventDefault()
148
+ const target = currentIndex >= 0 ? matchedItems[currentIndex] : matchedItems[0]
149
+ if (target) {
150
+ const titleEl = target.querySelector('.tree-item-title')
151
+ if (titleEl) {
152
+ titleEl.click()
153
+ }
154
+ }
155
+ }
108
156
  }
109
157
 
110
158
  handleCancelNew = () => {
@@ -298,8 +346,8 @@ export default class ItemListTree extends Component {
298
346
  onSelect = (
299
347
  e
300
348
  ) => {
301
- const id = e.target.getAttribute('data-item-id')
302
- const isGroup = e.target.getAttribute('data-is-group') === 'true'
349
+ const id = e.currentTarget.getAttribute('data-item-id')
350
+ const isGroup = e.currentTarget.getAttribute('data-is-group') === 'true'
303
351
  const { store } = window
304
352
  if (isGroup) {
305
353
  store.storeAssign({
@@ -329,6 +377,8 @@ export default class ItemListTree extends Component {
329
377
  <TreeSearch
330
378
  onSearch={this.handleChange}
331
379
  keyword={this.state.keyword}
380
+ autoFocus={this.props.autoFocus}
381
+ onKeyDown={this.handleKeyDown}
332
382
  />
333
383
  </div>
334
384
  )
@@ -717,6 +767,35 @@ export default class ItemListTree extends Component {
717
767
  renderGroup = (group, index, parentId) => {
718
768
  const pids = typeof parentId === 'string' ? parentId : ''
719
769
  const pid = pids + '#' + group.id
770
+ const { bookmarkIds = [], bookmarkGroupIds = [] } = group
771
+
772
+ const hasMatchedItems = (ids) => {
773
+ const tree = this.props.bookmarksMap
774
+ const { keyword } = this.state
775
+ return ids.some(id => {
776
+ const item = tree.get(id)
777
+ return item && createName(item).toLowerCase().includes(keyword.toLowerCase())
778
+ })
779
+ }
780
+
781
+ const hasMatchedSubGroup = (bg) => {
782
+ const bgIds = bg.bookmarkIds || []
783
+ const bgSubIds = bg.bookmarkGroupIds || []
784
+ if (hasMatchedItems(bgIds)) return true
785
+ return bgSubIds.some(sgid => {
786
+ const subBg = window.store.bookmarkGroupTree[sgid]
787
+ return subBg && hasMatchedSubGroup(subBg)
788
+ })
789
+ }
790
+
791
+ if (this.state.keyword) {
792
+ if (!hasMatchedItems(bookmarkIds) && !bookmarkGroupIds.some(id => {
793
+ const sg = window.store.bookmarkGroupTree[id]
794
+ return sg && hasMatchedSubGroup(sg)
795
+ })) {
796
+ return null
797
+ }
798
+ }
720
799
  return (
721
800
  <div key={group.id} className='group-container'>
722
801
  {
@@ -803,9 +882,14 @@ export default class ItemListTree extends Component {
803
882
  if (!shouldRender) {
804
883
  return null
805
884
  }
885
+ const subGroups = this.renderSubGroup(bookmarkGroupIds, parentId)
886
+ const childs = this.renderChilds(bookmarkIds, parentId)
887
+ if (this.state.keyword && subGroups.length === 0 && childs.length === 0) {
888
+ return null
889
+ }
806
890
  return [
807
- ...this.renderSubGroup(bookmarkGroupIds, parentId),
808
- ...this.renderChilds(bookmarkIds, parentId)
891
+ ...subGroups,
892
+ ...childs
809
893
  ]
810
894
  }
811
895
 
@@ -851,12 +935,33 @@ export default class ItemListTree extends Component {
851
935
  listStyle = {}
852
936
  } = this.props
853
937
  const level1Bookgroups = ready
854
- ? bookmarkGroups.filter(
855
- d => !d.level || d.level < 2
856
- )
938
+ ? bookmarkGroups.filter(d => {
939
+ if (!d.level || d.level < 2) {
940
+ if (this.state.keyword) {
941
+ const hasMatchedItemsRecursive = (bg) => {
942
+ const ids = bg.bookmarkIds || []
943
+ const subIds = bg.bookmarkGroupIds || []
944
+ const tree = this.props.bookmarksMap
945
+ const { keyword } = this.state
946
+ const hasMatch = ids.some(id => {
947
+ const item = tree.get(id)
948
+ return item && createName(item).toLowerCase().includes(keyword.toLowerCase())
949
+ })
950
+ if (hasMatch) return true
951
+ return subIds.some(sgid => {
952
+ const subBg = window.store.bookmarkGroupTree[sgid]
953
+ return subBg && hasMatchedItemsRecursive(subBg)
954
+ })
955
+ }
956
+ return hasMatchedItemsRecursive(d)
957
+ }
958
+ return true
959
+ }
960
+ return false
961
+ })
857
962
  : []
858
963
  return (
859
- <div className={`tree-list item-type-${type}`}>
964
+ <div className={`tree-list item-type-${type}`} ref={this.treeRef}>
860
965
  <div className='tree-list-header'>
861
966
  {
862
967
  staticList
@@ -18,6 +18,9 @@
18
18
  .tree-item-title
19
19
  font-weight bold
20
20
  font-size 14px
21
+
22
+ &.search-selected
23
+ outline 1px dashed var(--primary)
21
24
  &.selected
22
25
  &.item-dragover
23
26
  &:hover
@@ -3,7 +3,7 @@ import { debounce } from 'lodash-es'
3
3
  import Search from '../common/search'
4
4
  import runIdle from '../../common/run-idle'
5
5
 
6
- export default memo(function TreeSearchComponent ({ onSearch, keyword }) {
6
+ export default memo(function TreeSearchComponent ({ onSearch, keyword, autoFocus, onKeyDown }) {
7
7
  const [searchTerm, setSearchTerm] = useState(keyword)
8
8
 
9
9
  const performSearch = debounce((term) => {
@@ -18,11 +18,19 @@ export default memo(function TreeSearchComponent ({ onSearch, keyword }) {
18
18
  performSearch(term)
19
19
  }
20
20
 
21
+ const handleKeyDown = (e) => {
22
+ if (onKeyDown) {
23
+ onKeyDown(e)
24
+ }
25
+ }
26
+
21
27
  return (
22
28
  <Search
23
29
  onChange={handleChange}
30
+ onKeyDown={handleKeyDown}
24
31
  value={searchTerm}
25
32
  allowClear
33
+ autoFocus={autoFocus}
26
34
  />
27
35
  )
28
36
  })
@@ -5,6 +5,7 @@ import React, { useState, useEffect } from 'react'
5
5
  import { Form, Input, InputNumber, Switch, Select, Button, Tooltip, Alert } from 'antd'
6
6
  import { formItemLayout, tailFormItemLayout } from '../../common/form-layout'
7
7
  import HelpIcon from '../common/help-icon'
8
+ import BatchOpEditor from '../batch-op/batch-op-editor'
8
9
 
9
10
  export default function WidgetForm ({ widget, onSubmit, loading, hasRunningInstance }) {
10
11
  const [form] = Form.useForm()
@@ -33,6 +34,7 @@ export default function WidgetForm ({ widget, onSubmit, loading, hasRunningInsta
33
34
  const { info } = widget
34
35
  const { configs, type, singleInstance } = info
35
36
  const isInstanceWidget = type === 'instance'
37
+ const isFrontendWidget = type === 'frontend'
36
38
  const txt = isInstanceWidget ? 'Start widget' : 'Run widget'
37
39
  const isDisabled = loading || (singleInstance && hasRunningInstance)
38
40
 
@@ -112,6 +114,10 @@ export default function WidgetForm ({ widget, onSubmit, loading, hasRunningInsta
112
114
  return acc
113
115
  }, {})
114
116
 
117
+ if (isFrontendWidget && info.name === 'Batch Operation') {
118
+ return <BatchOpEditor widget={widget} />
119
+ }
120
+
115
121
  return (
116
122
  <div className='widget-form'>
117
123
  <div className='pd1b alignright'>
@@ -5,8 +5,8 @@
5
5
  import { refsStatic } from '../components/common/ref'
6
6
 
7
7
  export default Store => {
8
- Store.prototype.onCheckUpdate = (noSkip = true) => {
9
- refsStatic.get('upgrade')?.appUpdateCheck(noSkip)
8
+ Store.prototype.onCheckUpdate = (isManual = false) => {
9
+ refsStatic.get('upgrade')?.appUpdateCheck(isManual)
10
10
  }
11
11
  Store.prototype.getProxySetting = function () {
12
12
  const {
@@ -6,7 +6,6 @@ import handleError from '../common/error-handler'
6
6
  import Modal from '../components/common/modal'
7
7
  import { debounce, some, get, pickBy } from 'lodash-es'
8
8
  import {
9
- modals,
10
9
  leftSidebarWidthKey,
11
10
  rightSidebarWidthKey,
12
11
  addPanelWidthLsKey,
@@ -95,33 +94,6 @@ export default Store => {
95
94
  window.store['_' + name] = JSON.stringify(value)
96
95
  }
97
96
 
98
- Store.prototype.toggleBatchOp = function () {
99
- window.store.showModal = window.store.showModal === modals.batchOps ? modals.hide : modals.batchOps
100
- }
101
-
102
- Store.prototype.runBatchOp = function (path) {
103
- window.store.showModal = modals.batchOps
104
- async function updateText () {
105
- const text = await window.fs.readFile(path)
106
- refsStatic.get('batch-op')?.setState({
107
- text
108
- })
109
- }
110
- function queue () {
111
- refsStatic.get('batch-op')?.handleClick()
112
- }
113
- function run () {
114
- refsStatic.get('batch-op')?.handleExec()
115
- }
116
- try {
117
- setTimeout(updateText, 2000)
118
- setTimeout(queue, 3000)
119
- setTimeout(run, 4000)
120
- } catch (e) {
121
- console.error(e)
122
- }
123
- }
124
-
125
97
  Store.prototype.setSettingItem = function (v) {
126
98
  window.store.settingItem = v
127
99
  }
@@ -7,12 +7,12 @@ import parseInt10 from '../common/parse-int10'
7
7
  import { infoTabs, statusMap, defaultEnvLang } from '../common/constants'
8
8
  import fs from '../common/fs'
9
9
  import generate from '../common/id-with-stamp'
10
+ import { refsStatic } from '../components/common/ref'
10
11
  import defaultSettings from '../common/default-setting'
11
12
  import encodes from '../components/bookmark-form/common/encodes'
12
13
  import { initWsCommon } from '../common/fetch-from-server'
13
14
  import safeParse from '../common/parse-json-safe'
14
15
  import initWatch from './watch'
15
- import { refsStatic } from '../components/common/ref'
16
16
  import { parseQuickConnect } from '../common/parse-quick-connect'
17
17
 
18
18
  function getHost (argv, opts) {
@@ -108,8 +108,8 @@ export async function addTabFromCommandLine (store, opts) {
108
108
  ) {
109
109
  window.initFolder = options.initFolder
110
110
  }
111
- if (options && options.batchOp) {
112
- window.store.runBatchOp(options.batchOp)
111
+ if (options.batchOp) {
112
+ refsStatic.get('batch-op-runner')?.runBatchOpFromFile(options.batchOp)
113
113
  }
114
114
  }
115
115
 
@@ -625,7 +625,7 @@ export default Store => {
625
625
  throw new Error('remotePath is required')
626
626
  }
627
627
 
628
- window._transferConflictPolicy = args.conflictPolicy || 'overwrite'
628
+ window._transferConflictPolicy = args.conflictPolicy || 'mergeOrOverwriteAll'
629
629
 
630
630
  const fromFile = await getLocalFileInfo(localPath)
631
631
  const transferItem = {
@@ -670,7 +670,7 @@ export default Store => {
670
670
  throw new Error('localPath is required')
671
671
  }
672
672
 
673
- window._transferConflictPolicy = args.conflictPolicy || 'overwrite'
673
+ window._transferConflictPolicy = args.conflictPolicy || 'mergeOrOverwriteAll'
674
674
 
675
675
  const fromFile = await getRemoteFileInfo(sftp, remotePath)
676
676
  const transferItem = {
@@ -176,6 +176,30 @@ export default (Store) => {
176
176
  window.onSyncAll = false
177
177
  }
178
178
 
179
+ Store.prototype.downloadSettingAll = async function () {
180
+ const { store, onSyncAll } = window
181
+ if (store.autoSyncReady === false) {
182
+ return
183
+ }
184
+ if (onSyncAll) {
185
+ return
186
+ }
187
+ window.onSyncAll = true
188
+ const types = Object.keys(syncTypes)
189
+ for (const type of types) {
190
+ const gistId = store.getSyncGistId(type)
191
+ if (type === syncTypes.webdav) {
192
+ const serverUrl = get(window.store.config, 'syncSetting.webdavServerUrl')
193
+ if (serverUrl) {
194
+ await store.downloadSetting(type)
195
+ }
196
+ } else if (gistId) {
197
+ await store.downloadSetting(type)
198
+ }
199
+ }
200
+ window.onSyncAll = false
201
+ }
202
+
179
203
  Store.prototype.uploadSetting = async function (type) {
180
204
  if (window[type + 'IsSyncing']) {
181
205
  return false
@@ -561,7 +585,7 @@ export default (Store) => {
561
585
 
562
586
  Store.prototype.handleAutoSync = function (v) {
563
587
  const { store } = window
564
- store.setConfig({
588
+ store.updateSyncSetting({
565
589
  autoSync: v
566
590
  })
567
591
  }
@@ -509,7 +509,7 @@ export default Store => {
509
509
  'autoReConnect'
510
510
  ]
511
511
  const { history } = store
512
- const index = history.findIndex(d => {
512
+ const index = history.filter(d => d.id && d.tab).findIndex(d => {
513
513
  for (const key in tab) {
514
514
  if (tabPropertiesExcludes.includes(key)) {
515
515
  continue