mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-08-16 22:50:22 +03:00
Bundle archive-view
This commit is contained in:
parent
a764577f55
commit
7427639be0
@ -25,7 +25,7 @@
|
||||
"@atom/source-map-support": "^0.3.4",
|
||||
"@babel/core": "7.18.6",
|
||||
"about": "file:packages/about",
|
||||
"archive-view": "https://codeload.github.com/atom/archive-view/legacy.tar.gz/refs/tags/v0.66.0",
|
||||
"archive-view": "file:packages/archive-view",
|
||||
"async": "3.2.4",
|
||||
"atom-dark-syntax": "file:packages/atom-dark-syntax",
|
||||
"atom-dark-ui": "file:packages/atom-dark-ui",
|
||||
@ -187,7 +187,7 @@
|
||||
"solarized-dark-syntax": "file:./packages/solarized-dark-syntax",
|
||||
"solarized-light-syntax": "file:./packages/solarized-light-syntax",
|
||||
"about": "file:./packages/about",
|
||||
"archive-view": "0.66.0",
|
||||
"archive-view": "file:./packages/archive-view",
|
||||
"autocomplete-atom-api": "0.10.7",
|
||||
"autocomplete-css": "file:./packages/autocomplete-css",
|
||||
"autocomplete-html": "file:./packages/autocomplete-html",
|
||||
|
20
packages/archive-view/README.md
Normal file
20
packages/archive-view/README.md
Normal file
@ -0,0 +1,20 @@
|
||||
# Archive view package
|
||||
|
||||
Adds support for browsing archive files in Pulsar with the following extensions:
|
||||
|
||||
* `.egg`
|
||||
* `.epub`
|
||||
* `.jar`
|
||||
* `.love`
|
||||
* `.nupkg`
|
||||
* `.tar`
|
||||
* `.tar.gz`
|
||||
* `.tgz`
|
||||
* `.war`
|
||||
* `.whl`
|
||||
* `.xpi`
|
||||
* `.zip`
|
||||
|
||||
Select a file to extract it to a temp file and open it in a new editor.
|
||||
|
||||
![](./resources/preview.png)
|
3
packages/archive-view/keymaps/archive-view.cson
Normal file
3
packages/archive-view/keymaps/archive-view.cson
Normal file
@ -0,0 +1,3 @@
|
||||
'.archive-editor':
|
||||
'k': 'core:move-up'
|
||||
'j': 'core:move-down'
|
208
packages/archive-view/lib/archive-editor-view.js
Normal file
208
packages/archive-view/lib/archive-editor-view.js
Normal file
@ -0,0 +1,208 @@
|
||||
/** @babel */
|
||||
/** @jsx etch.dom */
|
||||
|
||||
import fs from 'fs'
|
||||
import humanize from 'humanize-plus'
|
||||
import archive from 'ls-archive'
|
||||
import {CompositeDisposable, Disposable, Emitter, File} from 'atom'
|
||||
import etch from 'etch'
|
||||
|
||||
import FileView from './file-view'
|
||||
import DirectoryView from './directory-view'
|
||||
|
||||
export default class ArchiveEditorView {
|
||||
constructor (archivePath) {
|
||||
this.disposables = new CompositeDisposable()
|
||||
this.emitter = new Emitter()
|
||||
this.path = archivePath
|
||||
this.file = new File(this.path)
|
||||
this.entries = []
|
||||
etch.initialize(this)
|
||||
|
||||
this.refresh()
|
||||
|
||||
this.disposables.add(this.file.onDidChange(() => this.refresh()))
|
||||
this.disposables.add(this.file.onDidRename(() => this.refresh()))
|
||||
this.disposables.add(this.file.onDidDelete(() => this.destroy()))
|
||||
|
||||
const focusHandler = () => this.focusSelectedFile()
|
||||
|
||||
this.element.addEventListener('focus', focusHandler)
|
||||
this.disposables.add(new Disposable(() => this.element.removeEventListener('focus', focusHandler)))
|
||||
}
|
||||
|
||||
update () {}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className='archive-editor' tabIndex='-1'>
|
||||
<div className='archive-container'>
|
||||
<div ref='loadingMessage' className='padded icon icon-hourglass text-info'>{`Loading archive\u2026`}</div>
|
||||
<div ref='errorMessage' className='padded icon icon-alert text-error' />
|
||||
<div className='inset-panel'>
|
||||
<div ref='summary' className='panel-heading' />
|
||||
<ol ref='tree' className='archive-tree padded list-tree has-collapsable-children' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
copy () {
|
||||
return new ArchiveEditorView(this.path)
|
||||
}
|
||||
|
||||
destroy () {
|
||||
while (this.entries.length > 0) {
|
||||
this.entries.pop().destroy()
|
||||
}
|
||||
this.disposables.dispose()
|
||||
this.emitter.emit('did-destroy')
|
||||
etch.destroy(this)
|
||||
}
|
||||
|
||||
onDidDestroy (callback) {
|
||||
return this.emitter.on('did-destroy', callback)
|
||||
}
|
||||
|
||||
onDidChangeTitle (callback) {
|
||||
return this.emitter.on('did-change-title', callback)
|
||||
}
|
||||
|
||||
serialize () {
|
||||
return {
|
||||
deserializer: this.constructor.name,
|
||||
path: this.path
|
||||
}
|
||||
}
|
||||
|
||||
getPath () {
|
||||
return this.file.getPath()
|
||||
}
|
||||
|
||||
getTitle () {
|
||||
return this.path ? this.file.getBaseName() : 'untitled'
|
||||
}
|
||||
|
||||
getURI () {
|
||||
return this.path
|
||||
}
|
||||
|
||||
refresh () {
|
||||
this.refs.summary.style.display = 'none'
|
||||
this.refs.tree.style.display = 'none'
|
||||
this.refs.loadingMessage.style.display = ''
|
||||
this.refs.errorMessage.style.display = 'none'
|
||||
|
||||
if (this.path !== this.getPath()) {
|
||||
this.path = this.getPath()
|
||||
this.emitter.emit('did-change-title')
|
||||
}
|
||||
|
||||
const originalPath = this.path
|
||||
archive.list(this.path, {tree: true}, (error, entries) => {
|
||||
if (originalPath !== this.path) {
|
||||
return
|
||||
}
|
||||
|
||||
if (error != null) {
|
||||
let message = 'Reading the archive file failed'
|
||||
if (error.message) {
|
||||
message += `: ${error.message}`
|
||||
}
|
||||
this.refs.errorMessage.style.display = ''
|
||||
this.refs.errorMessage.textContent = message
|
||||
} else {
|
||||
this.createTreeEntries(entries)
|
||||
this.updateSummary()
|
||||
}
|
||||
|
||||
// We hide the loading message _after_ creating the archive tree
|
||||
// to avoid forced reflows.
|
||||
this.refs.loadingMessage.style.display = 'none'
|
||||
})
|
||||
}
|
||||
|
||||
createTreeEntries (entries) {
|
||||
while (this.entries.length > 0) {
|
||||
this.entries.pop().destroy()
|
||||
}
|
||||
|
||||
let index = 0
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const entryView = new DirectoryView(this, index, this.path, entry)
|
||||
this.entries.push(entryView)
|
||||
} else {
|
||||
const entryView = new FileView(this, index, this.path, entry)
|
||||
this.entries.push(entryView)
|
||||
}
|
||||
index++
|
||||
}
|
||||
|
||||
this.selectFileAfterIndex(-1)
|
||||
|
||||
// Wait until selecting (focusing) the first file before appending the entries
|
||||
// to avoid a double-forced reflow when focusing.
|
||||
for (const entry of this.entries) {
|
||||
this.refs.tree.appendChild(entry.element)
|
||||
}
|
||||
|
||||
this.refs.tree.style.display = ''
|
||||
}
|
||||
|
||||
updateSummary () {
|
||||
const fileCount = this.entries.filter((entry) => entry instanceof FileView).length
|
||||
const fileLabel = fileCount === 1 ? '1 file' : `${humanize.intComma(fileCount)} files`
|
||||
|
||||
const directoryCount = this.entries.filter((entry) => entry instanceof DirectoryView).length
|
||||
const directoryLabel = directoryCount === 1 ? '1 folder' : `${humanize.intComma(directoryCount)} folders`
|
||||
|
||||
this.refs.summary.style.display = ''
|
||||
let fileSize
|
||||
try {
|
||||
fileSize = fs.statSync(this.path)?.size;
|
||||
} catch (e) {}
|
||||
if (fileSize == null) fileSize = -1
|
||||
this.refs.summary.textContent = `${humanize.fileSize(fileSize)} with ${fileLabel} and ${directoryLabel}`
|
||||
}
|
||||
|
||||
focusSelectedFile () {
|
||||
const selectedFile = this.refs.tree.querySelector('.selected')
|
||||
if (selectedFile) {
|
||||
selectedFile.focus()
|
||||
}
|
||||
}
|
||||
|
||||
selectFileBeforeIndex (index) {
|
||||
for (let i = index - 1; i >= 0; i--) {
|
||||
const previousEntry = this.entries[i]
|
||||
if (previousEntry instanceof FileView) {
|
||||
previousEntry.select()
|
||||
break
|
||||
} else {
|
||||
if (previousEntry.selectLastFile()) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectFileAfterIndex (index) {
|
||||
for (let i = index + 1; i < this.entries.length; i++) {
|
||||
const nextEntry = this.entries[i]
|
||||
if (nextEntry instanceof FileView) {
|
||||
nextEntry.select()
|
||||
break
|
||||
} else {
|
||||
if (nextEntry.selectFirstFile()) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
focus () {
|
||||
this.focusSelectedFile()
|
||||
}
|
||||
}
|
74
packages/archive-view/lib/archive-editor.js
Normal file
74
packages/archive-view/lib/archive-editor.js
Normal file
@ -0,0 +1,74 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const {Disposable} = require('atom')
|
||||
|
||||
const getIconServices = require('./get-icon-services')
|
||||
const ArchiveEditorView = require('./archive-editor-view')
|
||||
|
||||
module.exports = {
|
||||
activate () {
|
||||
this.disposable = atom.workspace.addOpener((filePath = '') => {
|
||||
// Check that filePath exists before opening, in case a remote URI was given
|
||||
if (!isPathSupported(filePath)) return;
|
||||
let isFile = false
|
||||
try {
|
||||
isFile = fs.statSync(filePath)?.isFile()
|
||||
} catch (e) {}
|
||||
if (isFile) {
|
||||
return new ArchiveEditorView(filePath)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
deactivate () {
|
||||
this.disposable.dispose()
|
||||
for (const item of atom.workspace.getPaneItems()) {
|
||||
if (item instanceof ArchiveEditorView) {
|
||||
item.destroy()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
consumeElementIcons (service) {
|
||||
getIconServices().setElementIcons(service)
|
||||
return new Disposable(() => getIconServices().resetElementIcons())
|
||||
},
|
||||
|
||||
consumeFileIcons (service) {
|
||||
getIconServices().setFileIcons(service)
|
||||
return new Disposable(() => getIconServices().resetFileIcons())
|
||||
},
|
||||
|
||||
deserialize (params = {}) {
|
||||
let isFile = false
|
||||
try {
|
||||
isFile = fs.statSync(params.path)?.isFile()
|
||||
} catch (e) {}
|
||||
if (isFile) {
|
||||
return new ArchiveEditorView(params.path)
|
||||
} else {
|
||||
console.warn(`Can't build ArchiveEditorView for path "${params.path}"; file no longer exists`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isPathSupported (filePath) {
|
||||
switch (path.extname(filePath)) {
|
||||
case '.egg':
|
||||
case '.epub':
|
||||
case '.jar':
|
||||
case '.love':
|
||||
case '.nupkg':
|
||||
case '.tar':
|
||||
case '.tgz':
|
||||
case '.war':
|
||||
case '.whl':
|
||||
case '.xpi':
|
||||
case '.zip':
|
||||
return true
|
||||
case '.gz':
|
||||
return path.extname(path.basename(filePath, '.gz')) === '.tar'
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
36
packages/archive-view/lib/default-file-icons.js
Normal file
36
packages/archive-view/lib/default-file-icons.js
Normal file
@ -0,0 +1,36 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
class DefaultFileIcons {
|
||||
iconClassForPath (filePath) {
|
||||
const extension = path.extname(filePath).toLowerCase()
|
||||
const base = path.basename(filePath, extension).toLowerCase();
|
||||
|
||||
let isSymbolicLinkSync = false
|
||||
try {
|
||||
fs.lstatSync(filePath)?.isSymbolicLink();
|
||||
} catch (e) {}
|
||||
if (isSymbolicLinkSync) return 'icon-file-symlink-file'
|
||||
|
||||
if (base === 'readme' && ['','.markdown','.md','.mdown','.mkd','.mkdown','.rmd','.ron'].includes(extension)) {
|
||||
return 'icon-book'
|
||||
}
|
||||
|
||||
if (['.bz2','.egg','.epub','.gem','.gz','.jar','.lz','.lzma','.lzo','.rar','.tar','.tgz','.war','.whl','.xpi','.xz','.z','.zip'].includes(extension)) {
|
||||
return 'icon-file-zip'
|
||||
}
|
||||
|
||||
if (['.gif','.ico','.jpeg','.jpg','.png','.tif','.tiff','.webp'].includes(extension)) {
|
||||
return 'icon-file-media'
|
||||
}
|
||||
|
||||
if (extension === ".pdf") return 'icon-file-pdf'
|
||||
|
||||
if (['.ds_store','.a','.exe','.o','.pyc','.pyo','.so','.woff'].includes(extension)) {
|
||||
return 'icon-file-binary'
|
||||
}
|
||||
return 'icon-file-text'
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new DefaultFileIcons()
|
132
packages/archive-view/lib/directory-view.js
Normal file
132
packages/archive-view/lib/directory-view.js
Normal file
@ -0,0 +1,132 @@
|
||||
/** @babel */
|
||||
|
||||
import {CompositeDisposable, Disposable} from 'atom'
|
||||
|
||||
import FileView from './file-view'
|
||||
import getIconServices from './get-icon-services'
|
||||
|
||||
export default class DirectoryView {
|
||||
constructor (parentView, indexInParentView, archivePath, entry) {
|
||||
this.disposables = new CompositeDisposable()
|
||||
this.entries = []
|
||||
this.parentView = parentView
|
||||
this.indexInParentView = indexInParentView
|
||||
this.element = document.createElement('li')
|
||||
this.element.classList.add('list-nested-item', 'entry')
|
||||
|
||||
const listItem = document.createElement('span')
|
||||
listItem.classList.add('list-item')
|
||||
|
||||
const clickHandler = (event) => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
this.element.classList.toggle('collapsed')
|
||||
}
|
||||
listItem.addEventListener('click', clickHandler)
|
||||
this.disposables.add(new Disposable(() => { listItem.removeEventListener('click', clickHandler) }))
|
||||
|
||||
const entrySpan = document.createElement('span')
|
||||
entrySpan.textContent = entry.getName()
|
||||
listItem.appendChild(entrySpan)
|
||||
this.element.appendChild(listItem)
|
||||
|
||||
this.entry = entry
|
||||
this.entrySpan = entrySpan
|
||||
getIconServices().updateDirectoryIcon(this)
|
||||
|
||||
this.entriesTree = document.createElement('ol')
|
||||
this.entriesTree.classList.add('list-tree')
|
||||
let index = 0
|
||||
for (const child of entry.children) {
|
||||
if (child.isDirectory()) {
|
||||
const entryView = new DirectoryView(this, index, archivePath, child)
|
||||
this.entries.push(entryView)
|
||||
this.entriesTree.appendChild(entryView.element)
|
||||
} else {
|
||||
const entryView = new FileView(this, index, archivePath, child)
|
||||
this.entries.push(entryView)
|
||||
this.entriesTree.appendChild(entryView.element)
|
||||
}
|
||||
|
||||
index++
|
||||
}
|
||||
this.element.appendChild(this.entriesTree)
|
||||
}
|
||||
|
||||
destroy () {
|
||||
if (this.iconDisposable) {
|
||||
this.iconDisposable.dispose()
|
||||
this.iconDisposable = null
|
||||
}
|
||||
|
||||
while (this.entries.length > 0) {
|
||||
this.entries.pop().destroy()
|
||||
}
|
||||
|
||||
this.disposables.dispose()
|
||||
this.element.remove()
|
||||
}
|
||||
|
||||
selectFileBeforeIndex (index) {
|
||||
for (let i = index - 1; i >= 0; i--) {
|
||||
const previousEntry = this.entries[i]
|
||||
if (previousEntry instanceof FileView) {
|
||||
previousEntry.select()
|
||||
return
|
||||
} else {
|
||||
if (previousEntry.selectLastFile()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.parentView.selectFileBeforeIndex(this.indexInParentView)
|
||||
}
|
||||
|
||||
selectFileAfterIndex (index) {
|
||||
for (let i = index + 1; i < this.entries.length; i++) {
|
||||
const nextEntry = this.entries[i]
|
||||
if (nextEntry instanceof FileView) {
|
||||
nextEntry.select()
|
||||
return
|
||||
} else {
|
||||
if (nextEntry.selectFirstFile()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.parentView.selectFileAfterIndex(this.indexInParentView)
|
||||
}
|
||||
|
||||
selectFirstFile () {
|
||||
for (const entry of this.entries) {
|
||||
if (entry instanceof FileView) {
|
||||
entry.select()
|
||||
return true
|
||||
} else {
|
||||
if (entry.selectFirstFile()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
selectLastFile () {
|
||||
for (var i = this.entries.length - 1; i >= 0; i--) {
|
||||
const entry = this.entries[i]
|
||||
if (entry instanceof FileView) {
|
||||
entry.select()
|
||||
return true
|
||||
} else {
|
||||
if (entry.selectLastFile()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
112
packages/archive-view/lib/file-view.js
Normal file
112
packages/archive-view/lib/file-view.js
Normal file
@ -0,0 +1,112 @@
|
||||
/** @babel */
|
||||
|
||||
import {CompositeDisposable, Disposable} from 'atom'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import temp from 'temp'
|
||||
import archive from 'ls-archive'
|
||||
|
||||
import getIconServices from './get-icon-services'
|
||||
|
||||
export default class FileView {
|
||||
constructor (parentView, indexInParentView, archivePath, entry) {
|
||||
this.disposables = new CompositeDisposable()
|
||||
this.parentView = parentView
|
||||
this.indexInParentView = indexInParentView
|
||||
this.archivePath = archivePath
|
||||
this.entry = entry
|
||||
|
||||
this.element = document.createElement('li')
|
||||
this.element.classList.add('list-item', 'entry')
|
||||
this.element.tabIndex = -1
|
||||
|
||||
this.name = document.createElement('span')
|
||||
getIconServices().updateFileIcon(this)
|
||||
this.name.textContent = this.entry.getName()
|
||||
this.element.appendChild(this.name)
|
||||
|
||||
const clickHandler = () => {
|
||||
this.select()
|
||||
this.openFile()
|
||||
}
|
||||
this.element.addEventListener('click', clickHandler)
|
||||
this.disposables.add(new Disposable(() => { this.element.removeEventListener('click', clickHandler) }))
|
||||
|
||||
this.disposables.add(atom.commands.add(this.element, {
|
||||
'core:confirm': () => {
|
||||
if (this.isSelected()) {
|
||||
this.openFile()
|
||||
}
|
||||
},
|
||||
|
||||
'core:move-down': () => {
|
||||
if (this.isSelected()) {
|
||||
this.parentView.selectFileAfterIndex(this.indexInParentView)
|
||||
}
|
||||
},
|
||||
|
||||
'core:move-up': () => {
|
||||
if (this.isSelected()) {
|
||||
this.parentView.selectFileBeforeIndex(this.indexInParentView)
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.disposables.dispose()
|
||||
this.element.remove()
|
||||
}
|
||||
|
||||
isSelected () {
|
||||
return this.element.classList.contains('selected')
|
||||
}
|
||||
|
||||
logError (message, error) {
|
||||
console.error(message, error.stack != null ? error.stack : error)
|
||||
}
|
||||
|
||||
openFile () {
|
||||
archive.readFile(this.archivePath, this.entry.getPath(), (error, contents) => {
|
||||
if (error != null) {
|
||||
this.logError(`Error reading: ${this.entry.getPath()} from ${this.archivePath}`, error)
|
||||
} else {
|
||||
temp.mkdir('atom-', (error, tempDirPath) => {
|
||||
if (error != null) {
|
||||
this.logError(`Error creating temp directory: ${tempDirPath}`, error)
|
||||
} else {
|
||||
const tempArchiveDirPath = path.join(tempDirPath, path.basename(this.archivePath))
|
||||
fs.mkdir(tempArchiveDirPath, {recursive:true}, error => {
|
||||
if (error != null) {
|
||||
this.logError(`Error creating archive directory ${tempArchiveDirPath}`, error)
|
||||
} else {
|
||||
const tempFilePath = path.join(tempArchiveDirPath, this.entry.getName())
|
||||
fs.writeFile(tempFilePath, contents, error => {
|
||||
if (error != null) {
|
||||
this.logError(`Error writing to ${tempFilePath}`, error)
|
||||
} else {
|
||||
atom.workspace.open(tempFilePath)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
select () {
|
||||
this.element.focus()
|
||||
|
||||
const archiveEditorElement = this.element.closest('.archive-editor')
|
||||
// On initial tree creation, it is not possible for any entries to be selected
|
||||
// (The entries also haven't been added to the DOM yet)
|
||||
if (archiveEditorElement) {
|
||||
for (const selected of archiveEditorElement.querySelectorAll('.selected')) {
|
||||
selected.classList.remove('selected')
|
||||
}
|
||||
}
|
||||
this.element.classList.add('selected')
|
||||
}
|
||||
}
|
72
packages/archive-view/lib/get-icon-services.js
Normal file
72
packages/archive-view/lib/get-icon-services.js
Normal file
@ -0,0 +1,72 @@
|
||||
const DefaultFileIcons = require('./default-file-icons')
|
||||
const {Emitter, CompositeDisposable} = require('atom')
|
||||
const path = require('path')
|
||||
|
||||
let iconServices
|
||||
module.exports = function getIconServices () {
|
||||
if (!iconServices) iconServices = new IconServices()
|
||||
return iconServices
|
||||
}
|
||||
|
||||
class IconServices {
|
||||
constructor () {
|
||||
this.emitter = new Emitter()
|
||||
this.elementIcons = null
|
||||
this.elementIconDisposables = new CompositeDisposable()
|
||||
this.fileIcons = DefaultFileIcons
|
||||
}
|
||||
|
||||
onDidChange (callback) {
|
||||
return this.emitter.on('did-change', callback)
|
||||
}
|
||||
|
||||
resetElementIcons () {
|
||||
this.setElementIcons(null)
|
||||
}
|
||||
|
||||
resetFileIcons () {
|
||||
this.setFileIcons(DefaultFileIcons)
|
||||
}
|
||||
|
||||
setElementIcons (service) {
|
||||
if (service !== this.elementIcons) {
|
||||
if (this.elementIconDisposables != null) {
|
||||
this.elementIconDisposables.dispose()
|
||||
}
|
||||
if (service) { this.elementIconDisposables = new CompositeDisposable() }
|
||||
this.elementIcons = service
|
||||
return this.emitter.emit('did-change')
|
||||
}
|
||||
}
|
||||
|
||||
setFileIcons (service) {
|
||||
if (service !== this.fileIcons) {
|
||||
this.fileIcons = service
|
||||
return this.emitter.emit('did-change')
|
||||
}
|
||||
}
|
||||
|
||||
updateDirectoryIcon (view) {
|
||||
view.entrySpan.classList.add('directory', 'icon', 'icon-file-directory')
|
||||
if (this.elementIcons) {
|
||||
view.iconDisposable = this.elementIcons(view.entrySpan, view.entry.path, {isDirectory: true})
|
||||
}
|
||||
}
|
||||
|
||||
updateFileIcon (view) {
|
||||
const nameClasses = ['file', 'icon']
|
||||
if (this.elementIcons) {
|
||||
const fullPath = path.join(view.archivePath, view.entry.path)
|
||||
const disposable = this.elementIcons(view.name, fullPath)
|
||||
view.disposables.add(disposable)
|
||||
this.elementIconDisposables.add(disposable)
|
||||
} else {
|
||||
let typeClass = this.fileIcons.iconClassForPath(view.entry.path, 'archive-view') || []
|
||||
if (!Array.isArray(typeClass) && typeClass) {
|
||||
typeClass = typeClass.toString().split(/\s+/g)
|
||||
}
|
||||
nameClasses.push(...typeClass)
|
||||
}
|
||||
view.name.classList.add(...nameClasses)
|
||||
}
|
||||
}
|
47
packages/archive-view/package.json
Normal file
47
packages/archive-view/package.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "archive-view",
|
||||
"version": "0.66.0",
|
||||
"description": "View the files and folders inside archive files",
|
||||
"main": "./lib/archive-editor",
|
||||
"dependencies": {
|
||||
"etch": "0.9.0",
|
||||
"humanize-plus": "~1.8.2",
|
||||
"ls-archive": "https://github.com/pulsar-edit/node-ls-archive/archive/refs/tags/v1.3.1.tar.gz",
|
||||
"temp": "~0.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"standard": "^11.0.1"
|
||||
},
|
||||
"standard": {
|
||||
"env": {
|
||||
"atomtest": true,
|
||||
"browser": true,
|
||||
"jasmine": true,
|
||||
"node": true
|
||||
},
|
||||
"globals": [
|
||||
"atom"
|
||||
]
|
||||
},
|
||||
"repository": "https://github.com/pulsar-edit/pulsar",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"atom": "*"
|
||||
},
|
||||
"deserializers": {
|
||||
"ArchiveEditor": "deserialize",
|
||||
"ArchiveEditorView": "deserialize"
|
||||
},
|
||||
"consumedServices": {
|
||||
"atom.file-icons": {
|
||||
"versions": {
|
||||
"1.0.0": "consumeFileIcons"
|
||||
}
|
||||
},
|
||||
"file-icons.element-icons": {
|
||||
"versions": {
|
||||
"1.0.0": "consumeElementIcons"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
BIN
packages/archive-view/resources/preview.png
Normal file
BIN
packages/archive-view/resources/preview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 134 KiB |
46
packages/archive-view/spec/archive-editor-spec.js
Normal file
46
packages/archive-view/spec/archive-editor-spec.js
Normal file
@ -0,0 +1,46 @@
|
||||
const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise} = require('./async-spec-helpers') // eslint-disable-line no-unused-vars
|
||||
|
||||
const path = require('path')
|
||||
const ArchiveEditor = require('../lib/archive-editor')
|
||||
const ArchiveEditorView = require('../lib/archive-editor-view')
|
||||
|
||||
describe('ArchiveEditor', () => {
|
||||
const tarPath = path.join(__dirname, 'fixtures', 'nested.tar')
|
||||
|
||||
// Don't log during specs
|
||||
beforeEach(() => spyOn(console, 'warn'))
|
||||
|
||||
describe('.deserialize', () => {
|
||||
it('returns undefined if no file exists at the given path', () => {
|
||||
const editor1 = new ArchiveEditorView(tarPath)
|
||||
const state = editor1.serialize()
|
||||
editor1.destroy()
|
||||
|
||||
const editor2 = ArchiveEditor.deserialize(state)
|
||||
expect(editor2).toBeDefined()
|
||||
editor2.destroy()
|
||||
|
||||
state.path = 'bogus'
|
||||
expect(ArchiveEditor.deserialize(state)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('.deactivate()', () => {
|
||||
it('removes all ArchiveEditorViews from the workspace and does not open any new ones', async () => {
|
||||
const getArchiveEditorViews = () => {
|
||||
return atom.workspace.getPaneItems().filter(item => item instanceof ArchiveEditorView)
|
||||
}
|
||||
await atom.packages.activatePackage('archive-view')
|
||||
await atom.workspace.open(path.join(__dirname, 'fixtures', 'nested.tar'))
|
||||
await atom.workspace.open(path.join(__dirname, 'fixtures', 'invalid.zip'))
|
||||
await atom.workspace.open()
|
||||
expect(getArchiveEditorViews().length).toBe(2)
|
||||
|
||||
await atom.packages.deactivatePackage('archive-view')
|
||||
expect(getArchiveEditorViews().length).toBe(0)
|
||||
|
||||
await atom.workspace.open(path.join(__dirname, 'fixtures', 'nested.tar'))
|
||||
expect(getArchiveEditorViews().length).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
276
packages/archive-view/spec/archive-editor-view-spec.js
Normal file
276
packages/archive-view/spec/archive-editor-view-spec.js
Normal file
@ -0,0 +1,276 @@
|
||||
const {Disposable, File} = require('atom')
|
||||
const getIconServices = require('../lib/get-icon-services')
|
||||
const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise} = require('./async-spec-helpers') // eslint-disable-line no-unused-vars
|
||||
|
||||
async function condition (handler) {
|
||||
if (jasmine.isSpy(window.setTimeout)) {
|
||||
jasmine.useRealClock()
|
||||
}
|
||||
return conditionPromise(handler)
|
||||
}
|
||||
|
||||
describe('ArchiveEditorView', () => {
|
||||
let archiveEditorView, onDidChangeCallback, onDidRenameCallback, onDidDeleteCallback
|
||||
|
||||
beforeEach(async () => {
|
||||
spyOn(File.prototype, 'onDidChange').andCallFake(function (callback) {
|
||||
if (/\.tar$/.test(this.getPath())) {
|
||||
onDidChangeCallback = callback
|
||||
}
|
||||
return new Disposable()
|
||||
})
|
||||
|
||||
spyOn(File.prototype, 'onDidRename').andCallFake(function (callback) {
|
||||
if (/\.tar$/.test(this.getPath())) {
|
||||
onDidRenameCallback = callback
|
||||
}
|
||||
return new Disposable()
|
||||
})
|
||||
|
||||
spyOn(File.prototype, 'onDidDelete').andCallFake(function (callback) {
|
||||
if (/\.tar$/.test(this.getPath())) {
|
||||
onDidDeleteCallback = callback
|
||||
}
|
||||
return new Disposable()
|
||||
})
|
||||
|
||||
await atom.packages.activatePackage('archive-view')
|
||||
archiveEditorView = await atom.workspace.open('nested.tar')
|
||||
})
|
||||
|
||||
describe('.constructor()', () => {
|
||||
it('displays the files and folders in the archive file', async () => {
|
||||
expect(archiveEditorView.element).toExist()
|
||||
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
|
||||
|
||||
const directoryElements = archiveEditorView.element.querySelectorAll('.directory')
|
||||
expect(directoryElements.length).toBe(6)
|
||||
expect(directoryElements[0].textContent).toBe('d1')
|
||||
expect(directoryElements[1].textContent).toBe('d2')
|
||||
expect(directoryElements[2].textContent).toBe('d3')
|
||||
expect(directoryElements[3].textContent).toBe('d4')
|
||||
expect(directoryElements[4].textContent).toBe('da')
|
||||
expect(directoryElements[5].textContent).toBe('db')
|
||||
|
||||
const fileElements = archiveEditorView.element.querySelectorAll('.file')
|
||||
expect(fileElements.length).toBe(3)
|
||||
expect(fileElements[0].textContent).toBe('f1.txt')
|
||||
expect(fileElements[1].textContent).toBe('f2.txt')
|
||||
expect(fileElements[2].textContent).toBe('fa.txt')
|
||||
})
|
||||
|
||||
it('selects the first file', async () => {
|
||||
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
|
||||
expect(archiveEditorView.element.querySelector('.selected').textContent).toBe('f1.txt')
|
||||
})
|
||||
})
|
||||
|
||||
describe('.copy()', () => {
|
||||
it('returns a new ArchiveEditorView for the same file', () => {
|
||||
const newArchiveView = archiveEditorView.copy()
|
||||
expect(newArchiveView.getPath()).toBe(archiveEditorView.getPath())
|
||||
})
|
||||
})
|
||||
|
||||
describe('archive summary', () => {
|
||||
beforeEach(async () => {
|
||||
await atom.workspace.open('multiple-entries.zip')
|
||||
archiveEditorView = atom.workspace.getActivePaneItem()
|
||||
jasmine.attachToDOM(atom.views.getView(atom.workspace))
|
||||
})
|
||||
|
||||
it('shows correct statistics', async () => {
|
||||
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
|
||||
const heading = archiveEditorView.element.querySelector('.inset-panel .panel-heading')
|
||||
expect(heading).not.toBe(null)
|
||||
expect(heading.textContent).toBe('704 bytes with 4 files and 1 folder')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when core:move-up/core:move-down is triggered', () => {
|
||||
let selectedEntry
|
||||
const dispatch = (command) => {
|
||||
atom.commands.dispatch(archiveEditorView.element.querySelector('.selected'), command)
|
||||
selectedEntry = archiveEditorView.element.querySelector('.selected').textContent
|
||||
return true
|
||||
}
|
||||
|
||||
it('selects the next/previous file', async () => {
|
||||
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
|
||||
expect(archiveEditorView.element).toBeDefined()
|
||||
dispatch('core:move-up') && expect(selectedEntry).toBe('f1.txt')
|
||||
dispatch('core:move-down') && expect(selectedEntry).toBe('f2.txt')
|
||||
dispatch('core:move-down') && expect(selectedEntry).toBe('fa.txt')
|
||||
dispatch('core:move-down') && expect(selectedEntry).toBe('fa.txt')
|
||||
dispatch('core:move-up') && expect(selectedEntry).toBe('f2.txt')
|
||||
dispatch('core:move-up') && expect(selectedEntry).toBe('f1.txt')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when a file is clicked', () => {
|
||||
it('copies the contents to a temp file and opens it in a new editor', async () => {
|
||||
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
|
||||
archiveEditorView.element.querySelectorAll('.file')[2].click()
|
||||
await condition(() => atom.workspace.getActivePane().getItems().length > 1)
|
||||
expect(atom.workspace.getActivePaneItem().getText()).toBe('hey there\n')
|
||||
expect(atom.workspace.getActivePaneItem().getTitle()).toBe('fa.txt')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when a directory is clicked', () => {
|
||||
it('collapses/expands itself', async () => {
|
||||
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
|
||||
let directory = archiveEditorView.element.querySelectorAll('.list-nested-item.entry')[0]
|
||||
expect(directory.classList.contains('collapsed')).toBeFalsy()
|
||||
directory.querySelector('.list-item').click()
|
||||
expect(directory.classList.contains('collapsed')).toBeTruthy()
|
||||
directory.querySelector('.list-item').click()
|
||||
expect(directory.classList.contains('collapsed')).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when core:confirm is triggered', () => {
|
||||
it('copies the contents to a temp file and opens it in a new editor', async () => {
|
||||
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
|
||||
atom.commands.dispatch(archiveEditorView.element.querySelector('.file'), 'core:confirm')
|
||||
await condition(() => atom.workspace.getActivePane().getItems().length > 1)
|
||||
expect(atom.workspace.getActivePaneItem().getText()).toBe('')
|
||||
expect(atom.workspace.getActivePaneItem().getTitle()).toBe('f1.txt')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the file is modified', () => {
|
||||
it('refreshes the view', async () => {
|
||||
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
|
||||
spyOn(archiveEditorView, 'refresh')
|
||||
onDidChangeCallback()
|
||||
expect(archiveEditorView.refresh).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the file is renamed', () => {
|
||||
it('refreshes the view and updates the title', async () => {
|
||||
spyOn(File.prototype, 'getPath').andReturn('nested-renamed.tar')
|
||||
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
|
||||
spyOn(archiveEditorView, 'refresh').andCallThrough()
|
||||
spyOn(archiveEditorView, 'getTitle')
|
||||
onDidRenameCallback()
|
||||
expect(archiveEditorView.refresh).toHaveBeenCalled()
|
||||
expect(archiveEditorView.getTitle).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the file is removed', () => {
|
||||
it('destroys the view', async () => {
|
||||
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
|
||||
expect(atom.workspace.getActivePane().getItems().length).toBe(1)
|
||||
onDidDeleteCallback()
|
||||
expect(atom.workspace.getActivePaneItem()).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the file is invalid', () => {
|
||||
beforeEach(async () => {
|
||||
await atom.workspace.open('invalid.zip')
|
||||
archiveEditorView = atom.workspace.getActivePaneItem()
|
||||
jasmine.attachToDOM(atom.views.getView(atom.workspace))
|
||||
})
|
||||
|
||||
it('shows the error', async () => {
|
||||
await condition(() => archiveEditorView.refs.errorMessage.offsetHeight > 0)
|
||||
expect(archiveEditorView.refs.errorMessage.textContent.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('FileIcons', () => {
|
||||
async function openFile () {
|
||||
await atom.workspace.open('file-icons.zip')
|
||||
archiveEditorView = atom.workspace.getActivePaneItem()
|
||||
jasmine.attachToDOM(atom.views.getView(atom.workspace))
|
||||
}
|
||||
|
||||
describe('Icon service', () => {
|
||||
const service = { iconClassForPath () {} }
|
||||
beforeEach(() => openFile())
|
||||
|
||||
it('provides a default service', () => {
|
||||
expect(getIconServices().fileIcons).toBeDefined()
|
||||
expect(getIconServices().fileIcons).not.toBeNull()
|
||||
})
|
||||
|
||||
it('allows the default to be overridden', () => {
|
||||
getIconServices().setFileIcons(service)
|
||||
expect(getIconServices().fileIcons).toBe(service)
|
||||
})
|
||||
|
||||
it('allows service to be reset without hassle', () => {
|
||||
getIconServices().setFileIcons(service)
|
||||
getIconServices().resetFileIcons()
|
||||
expect(getIconServices().fileIcons).not.toBe(service)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Class handling', () => {
|
||||
function findEntryContainingText (text) {
|
||||
for (const entry of archiveEditorView.element.querySelectorAll('.list-item.entry')) {
|
||||
if (entry.textContent.includes(text)) { return entry }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function checkMultiClass () {
|
||||
expect(findEntryContainingText('adobe.pdf').querySelector('.file.icon').className).toBe('file icon text pdf-icon document')
|
||||
expect(findEntryContainingText('spacer.gif').querySelector('.file.icon').className).toBe('file icon binary gif-icon image')
|
||||
expect(findEntryContainingText('font.ttf').querySelector('.file.icon').className).toBe('file icon binary ttf-icon font')
|
||||
}
|
||||
|
||||
it('displays default file-icons', async () => {
|
||||
await openFile()
|
||||
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
|
||||
expect(findEntryContainingText('adobe.pdf').querySelector('.file.icon.icon-file-pdf').length).not.toBe(0)
|
||||
expect(findEntryContainingText('spacer.gif').querySelector('.file.icon.icon-file-media').length).not.toBe(0)
|
||||
expect(findEntryContainingText('sunn.o').querySelector('.file.icon.icon-file-binary').length).not.toBe(0)
|
||||
})
|
||||
|
||||
it('allows multiple classes to be passed', async () => {
|
||||
getIconServices().setFileIcons({
|
||||
iconClassForPath: (path) => {
|
||||
switch (path.match(/\w*$/)[0]) {
|
||||
case 'pdf': return 'text pdf-icon document'
|
||||
case 'ttf': return 'binary ttf-icon font'
|
||||
case 'gif': return 'binary gif-icon image'
|
||||
}
|
||||
}
|
||||
})
|
||||
await openFile()
|
||||
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
|
||||
checkMultiClass()
|
||||
})
|
||||
|
||||
it('allows an array of classes to be passed', async () => {
|
||||
getIconServices().setFileIcons({
|
||||
iconClassForPath: (path) => {
|
||||
switch (path.match(/\w*$/)[0]) {
|
||||
case 'pdf': return ['text', 'pdf-icon', 'document']
|
||||
case 'ttf': return ['binary', 'ttf-icon', 'font']
|
||||
case 'gif': return ['binary', 'gif-icon', 'image']
|
||||
}
|
||||
}
|
||||
})
|
||||
await openFile()
|
||||
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
|
||||
checkMultiClass()
|
||||
})
|
||||
|
||||
it('identifies context to icon-service providers', async () => {
|
||||
getIconServices().setFileIcons({
|
||||
iconClassForPath: (path, context) => `icon-${context}`
|
||||
})
|
||||
await openFile()
|
||||
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
|
||||
const icons = findEntryContainingText('adobe.pdf').querySelectorAll('.file.icon-archive-view')
|
||||
expect(icons.length).not.toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
103
packages/archive-view/spec/async-spec-helpers.js
Normal file
103
packages/archive-view/spec/async-spec-helpers.js
Normal file
@ -0,0 +1,103 @@
|
||||
/** @babel */
|
||||
|
||||
export function beforeEach (fn) {
|
||||
global.beforeEach(function () {
|
||||
const result = fn()
|
||||
if (result instanceof Promise) {
|
||||
waitsForPromise(() => result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function afterEach (fn) {
|
||||
global.afterEach(function () {
|
||||
const result = fn()
|
||||
if (result instanceof Promise) {
|
||||
waitsForPromise(() => result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
['it', 'fit', 'ffit', 'fffit'].forEach(function (name) {
|
||||
module.exports[name] = function (description, fn) {
|
||||
if (fn === undefined) {
|
||||
global[name](description)
|
||||
return
|
||||
}
|
||||
|
||||
global[name](description, function () {
|
||||
const result = fn()
|
||||
if (result instanceof Promise) {
|
||||
waitsForPromise(() => result)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export async function conditionPromise (condition, description = 'anonymous condition') {
|
||||
const startTime = Date.now()
|
||||
|
||||
while (true) {
|
||||
await timeoutPromise(100)
|
||||
|
||||
if (await condition()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Date.now() - startTime > 5000) {
|
||||
throw new Error('Timed out waiting on ' + description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function timeoutPromise (timeout) {
|
||||
return new Promise(function (resolve) {
|
||||
global.setTimeout(resolve, timeout)
|
||||
})
|
||||
}
|
||||
|
||||
function waitsForPromise (fn) {
|
||||
const promise = fn()
|
||||
global.waitsFor('spec promise to resolve', function (done) {
|
||||
promise.then(done, function (error) {
|
||||
jasmine.getEnv().currentSpec.fail(error)
|
||||
done()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function emitterEventPromise (emitter, event, timeout = 15000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
reject(new Error(`Timed out waiting for '${event}' event`))
|
||||
}, timeout)
|
||||
emitter.once(event, () => {
|
||||
clearTimeout(timeoutHandle)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function promisify (original) {
|
||||
return function (...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
args.push((err, ...results) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(...results)
|
||||
}
|
||||
})
|
||||
|
||||
return original(...args)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function promisifySome (obj, fnNames) {
|
||||
const result = {}
|
||||
for (const fnName of fnNames) {
|
||||
result[fnName] = promisify(obj[fnName])
|
||||
}
|
||||
return result
|
||||
}
|
BIN
packages/archive-view/spec/fixtures/file-icons.zip
vendored
Normal file
BIN
packages/archive-view/spec/fixtures/file-icons.zip
vendored
Normal file
Binary file not shown.
1
packages/archive-view/spec/fixtures/invalid.zip
vendored
Normal file
1
packages/archive-view/spec/fixtures/invalid.zip
vendored
Normal file
@ -0,0 +1 @@
|
||||
invalid
|
BIN
packages/archive-view/spec/fixtures/multiple-entries.zip
vendored
Normal file
BIN
packages/archive-view/spec/fixtures/multiple-entries.zip
vendored
Normal file
Binary file not shown.
BIN
packages/archive-view/spec/fixtures/nested.tar
vendored
Normal file
BIN
packages/archive-view/spec/fixtures/nested.tar
vendored
Normal file
Binary file not shown.
24
packages/archive-view/styles/archive-view.less
Normal file
24
packages/archive-view/styles/archive-view.less
Normal file
@ -0,0 +1,24 @@
|
||||
@import "ui-variables";
|
||||
|
||||
.archive-editor {
|
||||
background-color: @inset-panel-background-color;
|
||||
overflow: auto;
|
||||
contain: strict;
|
||||
|
||||
.archive-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.inset-panel {
|
||||
border-width: 0;
|
||||
|
||||
.panel-heading {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.archive-tree {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
13
yarn.lock
13
yarn.lock
@ -2036,14 +2036,12 @@ aproba@^1.0.3:
|
||||
resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
|
||||
integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==
|
||||
|
||||
"archive-view@https://codeload.github.com/atom/archive-view/legacy.tar.gz/refs/tags/v0.66.0":
|
||||
"archive-view@file:packages/archive-view":
|
||||
version "0.66.0"
|
||||
resolved "https://codeload.github.com/atom/archive-view/legacy.tar.gz/refs/tags/v0.66.0#e4563bf7cf4d6916eee4a949ca260315e8b2fe06"
|
||||
dependencies:
|
||||
etch "0.9.0"
|
||||
fs-plus "^3.0.0"
|
||||
humanize-plus "~1.8.2"
|
||||
ls-archive "1.3.4"
|
||||
ls-archive "https://github.com/pulsar-edit/node-ls-archive/archive/refs/tags/v1.3.1.tar.gz"
|
||||
temp "~0.8.1"
|
||||
|
||||
archiver-utils@^2.1.0:
|
||||
@ -6269,10 +6267,9 @@ lru-cache@^6.0.0:
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
ls-archive@1.3.4:
|
||||
version "1.3.4"
|
||||
resolved "https://registry.yarnpkg.com/ls-archive/-/ls-archive-1.3.4.tgz#52150919dab1acb094cdcef9dde9c66934a4650f"
|
||||
integrity sha512-7GmjZOckV+gzm4PM1/LcWIsZIRsSkAVmIchoEf5xjquNKU0Ti5KUvGQ3dl/7VsbZIduMOPwRDXrvpo3LVJ0Pmg==
|
||||
"ls-archive@https://github.com/pulsar-edit/node-ls-archive/archive/refs/tags/v1.3.1.tar.gz":
|
||||
version "1.3.1"
|
||||
resolved "https://github.com/pulsar-edit/node-ls-archive/archive/refs/tags/v1.3.1.tar.gz#43baa626a66f9c904548741b5bdf2f8095fbc7d9"
|
||||
dependencies:
|
||||
async "~0.2.9"
|
||||
colors "~0.6.2"
|
||||
|
Loading…
Reference in New Issue
Block a user