Compare commits
6 Commits
v0.4.0
...
ffafbc87d0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffafbc87d0 | ||
|
|
f0fc4a7d30 | ||
|
|
e4a62e1197 | ||
|
|
19a5c4ad8a | ||
|
|
3d3b078e60 | ||
|
|
d4e91ea9a6 |
67
README.md
67
README.md
@@ -1,23 +1,19 @@
|
|||||||
# Web File Storage
|
# Web File Storage
|
||||||
|
|
||||||
The Python package installs a `cista` executable. Use `hatch shell` to initiate and install in a virtual environment, or `pip install` it on your system. Alternatively `hatch run cista` may be used to skip the shell step but stay virtual. `pip install hatch` first if needed.
|
Run directly from repository with Hatch (or use pip install as usual):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
hatch run cista -l :3000 /path/to/files
|
||||||
|
```
|
||||||
|
|
||||||
|
Settings incl. these arguments are stored to config file on the first startup and later `hatch run cista` is sufficient. If the `cista` script is missing, consider `pip install -e .` (within `hatch shell`) or some other trickery (known issue with installs made prior to adding the startup script).
|
||||||
|
|
||||||
Create your user account:
|
Create your user account:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cista --user admin --privileged
|
hatch run cista --user admin --privileged
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running the server
|
|
||||||
|
|
||||||
Serve your files on localhost:8000:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cista -l :8000 /path/to/files
|
|
||||||
```
|
|
||||||
|
|
||||||
The Git repository does not contain a frontend build, so you should first do that...
|
|
||||||
|
|
||||||
## Build frontend
|
## Build frontend
|
||||||
|
|
||||||
Frontend needs to be built before using and after any frontend changes:
|
Frontend needs to be built before using and after any frontend changes:
|
||||||
@@ -29,50 +25,3 @@ npm run build
|
|||||||
```
|
```
|
||||||
|
|
||||||
This will place the front in `cista/wwwroot` from where the backend server delivers it, and that also gets included in the Python package built via `hatch build`.
|
This will place the front in `cista/wwwroot` from where the backend server delivers it, and that also gets included in the Python package built via `hatch build`.
|
||||||
|
|
||||||
## Development setup
|
|
||||||
|
|
||||||
For rapid turnaround during development, you should run `npm run dev` Vite development server on the Vue frontend. While that is running, start the backend on another terminal `hatch run cista --dev -l :8000` and connect to the frontend.
|
|
||||||
|
|
||||||
The backend and the frontend will each reload automatically at any code or config changes.
|
|
||||||
|
|
||||||
## System deployment
|
|
||||||
|
|
||||||
Clone the repository to `/srv/cista/cista-storage` or other suitable location accessible to the storage user account you plan to use. `sudo -u storage -s` and build the frontend if you hadn't already.
|
|
||||||
|
|
||||||
Create **/etc/systemd/system/cista@.service**:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[Unit]
|
|
||||||
Description=Cista storage %i
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
User=storage
|
|
||||||
WorkingDirectory=/srv/cista/cista-storage
|
|
||||||
ExecStart=hatch run cista -c /srv/cista/%i -l /srv/cista/%i/socket /media/storage/@%i/
|
|
||||||
TimeoutStopSec=2
|
|
||||||
Restart=always
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
```
|
|
||||||
|
|
||||||
This assumes you may want to run multiple separate storages, each having their files under `/media/storage/<domain>` and configuration under `/srv/cista/<domain>/`. Instead of numeric ports, we use UNIX sockets for convenience.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl enable --now cista@foo.example.com
|
|
||||||
systemctl enable --now cista@bar.example.com
|
|
||||||
```
|
|
||||||
|
|
||||||
Exposing this publicly online is the most convenient using the [Caddy](https://caddyserver.com/) web server but you can of course use Nginx or others as well. Or even run the server with `-l domain.example.com` given TLS certificates in the config folder.
|
|
||||||
|
|
||||||
**/etc/caddy/Caddyfile**:
|
|
||||||
|
|
||||||
```Caddyfile
|
|
||||||
foo.example.com, bar.example.com {
|
|
||||||
reverse_proxy unix//srv/cista/{host}/socket
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Using the `{host}` placeholder we can just put all the domains on the same block. That's the full server configuration you need. `systemctl enable --now caddy` or `systemctl restart caddy` for the config to take effect.
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
aria-label="Breadcrumb"
|
aria-label="Breadcrumb"
|
||||||
@keyup.left.stop="move(-1)"
|
@keyup.left.stop="move(-1)"
|
||||||
@keyup.right.stop="move(1)"
|
@keyup.right.stop="move(1)"
|
||||||
@keyup.enter="move(0)"
|
@focus="move(0)"
|
||||||
>
|
>
|
||||||
<a href="#/"
|
<a href="#/"
|
||||||
:ref="el => setLinkRef(0, el)"
|
:ref="el => setLinkRef(0, el)"
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
<th class="selection">
|
<th class="selection">
|
||||||
<input type="checkbox" tabindex="-1" v-model="allSelected" :indeterminate="selectionIndeterminate">
|
<input type="checkbox" tabindex="-1" v-model="allSelected" :indeterminate="selectionIndeterminate">
|
||||||
</th>
|
</th>
|
||||||
<th class="sortcolumn" :class="{ sortactive: store.sortOrder === 'name' }" @click="store.toggleSort('name')">Name</th>
|
<th class="sortcolumn" :class="{ sortactive: sort === 'name' }" @click="toggleSort('name')">Name</th>
|
||||||
<th class="sortcolumn modified right" :class="{ sortactive: store.sortOrder === 'modified' }" @click="store.toggleSort('modified')">Modified</th>
|
<th class="sortcolumn modified right" :class="{ sortactive: sort === 'modified' }" @click="toggleSort('modified')">Modified</th>
|
||||||
<th class="sortcolumn size right" :class="{ sortactive: store.sortOrder === 'size' }" @click="store.toggleSort('size')">Size</th>
|
<th class="sortcolumn size right" :class="{ sortactive: sort === 'size' }" @click="toggleSort('size')">Size</th>
|
||||||
<th class="menu"></th>
|
<th class="menu"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<FileSize :doc=editing />
|
<FileSize :doc=editing />
|
||||||
<td class="menu"></td>
|
<td class="menu"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-for="(doc, index) in documents" :key="doc.key">
|
<template v-for="(doc, index) in sortedDocuments" :key="doc.key">
|
||||||
<tr class="folder-change" v-if="showFolderBreadcrumb(index)">
|
<tr class="folder-change" v-if="showFolderBreadcrumb(index)">
|
||||||
<th colspan="5"><BreadCrumb :path="doc.loc ? doc.loc.split('/') : []" /></th>
|
<th colspan="5"><BreadCrumb :path="doc.loc ? doc.loc.split('/') : []" /></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -84,10 +84,8 @@ import { useMainStore } from '@/stores/main'
|
|||||||
import { Doc } from '@/repositories/Document'
|
import { Doc } from '@/repositories/Document'
|
||||||
import FileRenameInput from './FileRenameInput.vue'
|
import FileRenameInput from './FileRenameInput.vue'
|
||||||
import { connect, controlUrl } from '@/repositories/WS'
|
import { connect, controlUrl } from '@/repositories/WS'
|
||||||
import { formatSize } from '@/utils'
|
import { collator, formatSize } from '@/utils'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import ContextMenu from '@imengyu/vue3-context-menu'
|
|
||||||
import type { SortOrder } from '@/utils/docsort'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
path: Array<string>
|
path: Array<string>
|
||||||
@@ -122,6 +120,12 @@ const rename = (doc: Doc, newName: string) => {
|
|||||||
}
|
}
|
||||||
doc.name = newName // We should get an update from watch but this is quicker
|
doc.name = newName // We should get an update from watch but this is quicker
|
||||||
}
|
}
|
||||||
|
const sortedDocuments = computed(() => sorted(props.documents))
|
||||||
|
const showFolderBreadcrumb = (i: number) => {
|
||||||
|
const docs = sortedDocuments.value
|
||||||
|
const docloc = docs[i].loc
|
||||||
|
return i === 0 ? docloc !== loc.value : docloc !== docs[i - 1].loc
|
||||||
|
}
|
||||||
defineExpose({
|
defineExpose({
|
||||||
newFolder() {
|
newFolder() {
|
||||||
const now = Math.floor(Date.now() / 1000)
|
const now = Math.floor(Date.now() / 1000)
|
||||||
@@ -139,8 +143,8 @@ defineExpose({
|
|||||||
allSelected.value = !allSelected.value
|
allSelected.value = !allSelected.value
|
||||||
},
|
},
|
||||||
toggleSortColumn(column: number) {
|
toggleSortColumn(column: number) {
|
||||||
const order = ['', 'name', 'modified', 'size', ''][column]
|
const columns = ['', 'name', 'modified', 'size', '']
|
||||||
if (order) store.toggleSort(order as SortOrder)
|
toggleSort(columns[column])
|
||||||
},
|
},
|
||||||
isCursor() {
|
isCursor() {
|
||||||
return cursor.value !== null && editing.value === null
|
return cursor.value !== null && editing.value === null
|
||||||
@@ -160,25 +164,25 @@ defineExpose({
|
|||||||
},
|
},
|
||||||
cursorMove(d: number, select = false) {
|
cursorMove(d: number, select = false) {
|
||||||
// Move cursor up or down (keyboard navigation)
|
// Move cursor up or down (keyboard navigation)
|
||||||
const docs = props.documents
|
const documents = sortedDocuments.value
|
||||||
if (docs.length === 0) {
|
if (documents.length === 0) {
|
||||||
cursor.value = null
|
cursor.value = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const N = docs.length
|
const N = documents.length
|
||||||
const mod = (a: number, b: number) => ((a % b) + b) % b
|
const mod = (a: number, b: number) => ((a % b) + b) % b
|
||||||
const increment = (i: number, d: number) => mod(i + d, N + 1)
|
const increment = (i: number, d: number) => mod(i + d, N + 1)
|
||||||
const index =
|
const index =
|
||||||
cursor.value !== null ? docs.indexOf(cursor.value) : docs.length
|
cursor.value !== null ? documents.indexOf(cursor.value) : documents.length
|
||||||
const moveto = increment(index, d)
|
const moveto = increment(index, d)
|
||||||
cursor.value = docs[moveto] ?? null
|
cursor.value = documents[moveto] ?? null
|
||||||
const tr = cursor.value ? document.getElementById(`file-${cursor.value.key}`) : null
|
const tr = cursor.value ? document.getElementById(`file-${cursor.value.key}`) : null
|
||||||
if (select) {
|
if (select) {
|
||||||
// Go forwards, possibly wrapping over the end; the last entry is not toggled
|
// Go forwards, possibly wrapping over the end; the last entry is not toggled
|
||||||
let [begin, end] = d > 0 ? [index, moveto] : [moveto, index]
|
let [begin, end] = d > 0 ? [index, moveto] : [moveto, index]
|
||||||
for (let p = begin; p !== end; p = increment(p, 1)) {
|
for (let p = begin; p !== end; p = increment(p, 1)) {
|
||||||
if (p === N) continue
|
if (p === N) continue
|
||||||
const key = docs[p].key
|
const key = documents[p].key
|
||||||
if (store.selected.has(key)) store.selected.delete(key)
|
if (store.selected.has(key)) store.selected.delete(key)
|
||||||
else store.selected.add(key)
|
else store.selected.add(key)
|
||||||
}
|
}
|
||||||
@@ -249,10 +253,22 @@ const mkdir = (doc: Doc, name: string) => {
|
|||||||
doc.name = name
|
doc.name = name
|
||||||
doc.key = crypto.randomUUID()
|
doc.key = crypto.randomUUID()
|
||||||
}
|
}
|
||||||
const showFolderBreadcrumb = (i: number) => {
|
|
||||||
const docs = props.documents
|
// Column sort
|
||||||
const docloc = docs[i].loc
|
const toggleSort = (name: string) => {
|
||||||
return i === 0 ? docloc !== loc.value : docloc !== docs[i - 1].loc
|
sort.value = sort.value === name ? '' : name
|
||||||
|
}
|
||||||
|
const sort = ref<string>('')
|
||||||
|
const sortCompare = {
|
||||||
|
name: (a: Doc, b: Doc) => collator.compare(a.name, b.name),
|
||||||
|
modified: (a: Doc, b: Doc) => b.mtime - a.mtime,
|
||||||
|
size: (a: Doc, b: Doc) => b.size - a.size
|
||||||
|
}
|
||||||
|
const sorted = (documents: Doc[]) => {
|
||||||
|
const cmp = sortCompare[sort.value as keyof typeof sortCompare]
|
||||||
|
const sorted = [...documents]
|
||||||
|
if (cmp) sorted.sort(cmp)
|
||||||
|
return sorted
|
||||||
}
|
}
|
||||||
const selectionIndeterminate = computed({
|
const selectionIndeterminate = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
@@ -286,13 +302,9 @@ const allSelected = computed({
|
|||||||
|
|
||||||
const loc = computed(() => props.path.join('/'))
|
const loc = computed(() => props.path.join('/'))
|
||||||
|
|
||||||
const contextMenu = (ev: MouseEvent, doc: Doc) => {
|
const contextMenu = (ev: Event, doc: Doc) => {
|
||||||
cursor.value = doc
|
cursor.value = doc
|
||||||
ContextMenu.showContextMenu({
|
console.log('Context menu', ev, doc)
|
||||||
x: ev.x, y: ev.y, items: [
|
|
||||||
{ label: 'Rename', onClick: () => { editing.value = doc } },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
ref="search"
|
ref="search"
|
||||||
type="search"
|
type="search"
|
||||||
:value="query"
|
:value="query"
|
||||||
|
@blur="ev => { if (!query) closeSearch(ev) }"
|
||||||
@input="updateSearch"
|
@input="updateSearch"
|
||||||
placeholder="Search words"
|
placeholder="Search words"
|
||||||
class="margin-input"
|
class="margin-input"
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
import type { FileEntry, FUID, SelectedItems } from '@/repositories/Document'
|
|
||||||
import { Doc } from '@/repositories/Document'
|
|
||||||
import { defineStore } from 'pinia'
|
|
||||||
import { collator } from '@/utils'
|
|
||||||
import { logoutUser } from '@/repositories/User'
|
|
||||||
import { watchConnect } from '@/repositories/WS'
|
|
||||||
import { shallowRef } from 'vue'
|
|
||||||
import { sorted, type SortOrder } from '@/utils/docsort'
|
|
||||||
|
|
||||||
type User = {
|
|
||||||
username: string
|
|
||||||
privileged: boolean
|
|
||||||
isOpenLoginModal: boolean
|
|
||||||
isLoggedIn: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useMainStore = defineStore({
|
|
||||||
id: 'main',
|
|
||||||
state: () => ({
|
|
||||||
document: shallowRef<Doc[]>([]),
|
|
||||||
selected: new Set<FUID>(),
|
|
||||||
query: '' as string,
|
|
||||||
fileExplorer: null as any,
|
|
||||||
error: '' as string,
|
|
||||||
connected: false,
|
|
||||||
server: {} as Record<string, any>,
|
|
||||||
prefs: {
|
|
||||||
sortListing: '' as SortOrder,
|
|
||||||
sortFiltered: '' as SortOrder,
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
username: '',
|
|
||||||
privileged: false,
|
|
||||||
isLoggedIn: false,
|
|
||||||
isOpenLoginModal: false
|
|
||||||
} as User
|
|
||||||
}),
|
|
||||||
persist: {
|
|
||||||
paths: ['prefs'],
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
updateRoot(root: FileEntry[]) {
|
|
||||||
const docs = []
|
|
||||||
let loc = [] as string[]
|
|
||||||
for (const [level, name, key, mtime, size, isfile] of root) {
|
|
||||||
loc = loc.slice(0, level - 1)
|
|
||||||
docs.push(new Doc({
|
|
||||||
name,
|
|
||||||
loc: level ? loc.join('/') : '/',
|
|
||||||
key,
|
|
||||||
size,
|
|
||||||
mtime,
|
|
||||||
dir: !isfile,
|
|
||||||
}))
|
|
||||||
loc.push(name)
|
|
||||||
}
|
|
||||||
this.document = docs
|
|
||||||
},
|
|
||||||
login(username: string, privileged: boolean) {
|
|
||||||
this.user.username = username
|
|
||||||
this.user.privileged = privileged
|
|
||||||
this.user.isLoggedIn = true
|
|
||||||
this.user.isOpenLoginModal = false
|
|
||||||
if (!this.connected) watchConnect()
|
|
||||||
},
|
|
||||||
loginDialog() {
|
|
||||||
this.user.isOpenLoginModal = true
|
|
||||||
},
|
|
||||||
async logout() {
|
|
||||||
console.log("Logout")
|
|
||||||
await logoutUser()
|
|
||||||
this.$reset()
|
|
||||||
localStorage.clear()
|
|
||||||
history.go() // Reload page
|
|
||||||
},
|
|
||||||
toggleSort(name: SortOrder) {
|
|
||||||
if (this.query) this.prefs.sortFiltered = this.prefs.sortFiltered === name ? '' : name
|
|
||||||
else this.prefs.sortListing = this.prefs.sortListing === name ? '' : name
|
|
||||||
},
|
|
||||||
},
|
|
||||||
getters: {
|
|
||||||
sortOrder(): SortOrder { return this.query ? this.prefs.sortFiltered : this.prefs.sortListing },
|
|
||||||
isUserLogged(): boolean { return this.user.isLoggedIn },
|
|
||||||
recentDocuments(): Doc[] { return sorted(this.document, 'modified') },
|
|
||||||
selectedFiles(): SelectedItems {
|
|
||||||
const selected = this.selected
|
|
||||||
const found = new Set<FUID>()
|
|
||||||
const ret: SelectedItems = {
|
|
||||||
missing: new Set(),
|
|
||||||
docs: {},
|
|
||||||
keys: [],
|
|
||||||
recursive: [],
|
|
||||||
}
|
|
||||||
for (const doc of this.document) {
|
|
||||||
if (selected.has(doc.key)) {
|
|
||||||
found.add(doc.key)
|
|
||||||
ret.keys.push(doc.key)
|
|
||||||
ret.docs[doc.key] = doc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// What did we not select?
|
|
||||||
for (const key of selected) if (!found.has(key)) ret.missing.add(key)
|
|
||||||
// Build a flat list including contents recursively
|
|
||||||
const relnames = new Set<string>()
|
|
||||||
function add(rel: string, full: string, doc: Doc) {
|
|
||||||
if (!doc.dir && relnames.has(rel)) throw Error(`Multiple selections conflict for: ${rel}`)
|
|
||||||
relnames.add(rel)
|
|
||||||
ret.recursive.push([rel, full, doc])
|
|
||||||
}
|
|
||||||
for (const key of ret.keys) {
|
|
||||||
const base = ret.docs[key]
|
|
||||||
const basepath = base.loc ? `${base.loc}/${base.name}` : base.name
|
|
||||||
const nremove = base.loc.length
|
|
||||||
add(base.name, basepath, base)
|
|
||||||
for (const doc of this.document) {
|
|
||||||
if (doc.loc === basepath || doc.loc.startsWith(basepath) && doc.loc[basepath.length] === '/') {
|
|
||||||
const full = doc.loc ? `${doc.loc}/${doc.name}` : doc.name
|
|
||||||
const rel = full.slice(nremove)
|
|
||||||
add(rel, full, doc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Sort by rel (name stored as on download)
|
|
||||||
ret.recursive.sort((a, b) => collator.compare(a[0], b[0]))
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { Doc } from '@/repositories/Document'
|
|
||||||
import { collator } from '@/utils'
|
|
||||||
|
|
||||||
export const ordering = {
|
|
||||||
name: (a: Doc, b: Doc) => collator.compare(a.name, b.name),
|
|
||||||
modified: (a: Doc, b: Doc) => b.mtime - a.mtime,
|
|
||||||
size: (a: Doc, b: Doc) => b.size - a.size
|
|
||||||
}
|
|
||||||
export type SortOrder = keyof typeof ordering | ''
|
|
||||||
export const sorted = (documents: Doc[], order: SortOrder) => {
|
|
||||||
if (!order) return documents
|
|
||||||
const sorted = [...documents]
|
|
||||||
sorted.sort(ordering[order])
|
|
||||||
return sorted
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,6 @@ import { watchEffect, ref, computed } from 'vue'
|
|||||||
import { useMainStore } from '@/stores/main'
|
import { useMainStore } from '@/stores/main'
|
||||||
import Router from '@/router/index'
|
import Router from '@/router/index'
|
||||||
import { needleFormat, localeIncludes, collator } from '@/utils';
|
import { needleFormat, localeIncludes, collator } from '@/utils';
|
||||||
import { sorted } from '@/utils/docsort';
|
|
||||||
|
|
||||||
const store = useMainStore()
|
const store = useMainStore()
|
||||||
const fileExplorer = ref()
|
const fileExplorer = ref()
|
||||||
@@ -25,10 +24,7 @@ const documents = computed(() => {
|
|||||||
const loc = props.path.join('/')
|
const loc = props.path.join('/')
|
||||||
const query = props.query
|
const query = props.query
|
||||||
// List the current location
|
// List the current location
|
||||||
if (!query) return sorted(
|
if (!query) return store.document.filter(doc => doc.loc === loc)
|
||||||
store.document.filter(doc => doc.loc === loc),
|
|
||||||
store.prefs.sortListing,
|
|
||||||
)
|
|
||||||
// Find up to 100 newest documents that match the search
|
// Find up to 100 newest documents that match the search
|
||||||
const needle = needleFormat(query)
|
const needle = needleFormat(query)
|
||||||
let limit = 100
|
let limit = 100
|
||||||
@@ -39,11 +35,8 @@ const documents = computed(() => {
|
|||||||
if (--limit === 0) break
|
if (--limit === 0) break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Organize by folder, by relevance
|
||||||
const locsub = loc + '/'
|
const locsub = loc + '/'
|
||||||
// Custom sort override in effect?
|
|
||||||
const order = store.prefs.sortFiltered
|
|
||||||
if (order) return sorted(docs, order)
|
|
||||||
// Sort by relevance - current folder, then subfolders, then others
|
|
||||||
docs.sort((a, b) => (
|
docs.sort((a, b) => (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
(b.loc === loc) - (a.loc === loc) ||
|
(b.loc === loc) - (a.loc === loc) ||
|
||||||
@@ -61,6 +54,5 @@ const documents = computed(() => {
|
|||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
store.fileExplorer = fileExplorer.value
|
store.fileExplorer = fileExplorer.value
|
||||||
store.query = props.query
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://git.zi.fi/Vasanko/cista-storage"
|
Homepage = ""
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
cista = "cista.__main__:main"
|
cista = "cista.__main__:main"
|
||||||
|
|||||||
Reference in New Issue
Block a user