Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5df2308bdb | ||
|
|
f4c44ce1aa | ||
|
|
49232f11cc | ||
|
|
1258eff42d | ||
|
|
718d46e3f9 | ||
|
|
92d9c40a28 | ||
|
|
4f646fb344 | ||
|
|
d6304d0029 | ||
|
|
77e35cf0fc |
+1
-1
@@ -86,7 +86,7 @@ async def log_access(req, res):
|
|||||||
path = f"{path}?{qs}"
|
path = f"{path}?{qs}"
|
||||||
extra = getattr(req.ctx, "log_extra", None)
|
extra = getattr(req.ctx, "log_extra", None)
|
||||||
line = format_access_log(
|
line = format_access_log(
|
||||||
client, res.status, req.method, host, path, duration_ms, extra=extra
|
client, res.status, req.method, host, path, duration_ms=duration_ms, extra=extra
|
||||||
)
|
)
|
||||||
access_logger.info(line)
|
access_logger.info(line)
|
||||||
return res
|
return res
|
||||||
|
|||||||
+1
-3
@@ -1293,9 +1293,7 @@ def _token_belongs_to_user(token, username, sso_user_id):
|
|||||||
|
|
||||||
def _is_anonymous_share_token(token: config.Token) -> bool:
|
def _is_anonymous_share_token(token: config.Token) -> bool:
|
||||||
return (
|
return (
|
||||||
sharefs.is_share_token(token)
|
sharefs.is_share_token(token) and not token.username and not token.sso_user_id
|
||||||
and not token.username
|
|
||||||
and not token.sso_user_id
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -145,6 +145,9 @@ class _PreviewWorker:
|
|||||||
|
|
||||||
async def kill(self) -> None:
|
async def kill(self) -> None:
|
||||||
if self.proc.returncode is None:
|
if self.proc.returncode is None:
|
||||||
|
# Safe to hard-kill: the worker is stateless per request, and its
|
||||||
|
# subprocesses (ffmpeg) use stdin=DEVNULL so they never hold the
|
||||||
|
# worker's pipes open — proc.wait() cannot hang on pipe EOF.
|
||||||
with contextlib.suppress(ProcessLookupError):
|
with contextlib.suppress(ProcessLookupError):
|
||||||
self.proc.kill()
|
self.proc.kill()
|
||||||
await self.proc.wait()
|
await self.proc.wait()
|
||||||
@@ -179,7 +182,6 @@ class _PreviewWorkerPool:
|
|||||||
stdin=asyncio.subprocess.PIPE,
|
stdin=asyncio.subprocess.PIPE,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
start_new_session=True,
|
|
||||||
)
|
)
|
||||||
_active_procs.add(proc)
|
_active_procs.add(proc)
|
||||||
try:
|
try:
|
||||||
|
|||||||
+13
-6
@@ -219,7 +219,18 @@ def _image_via_ffmpeg(path: Path, maxsize: int, quality: int) -> bytes:
|
|||||||
cmd.insert(5, f"{new_w}x{new_h}")
|
cmd.insert(5, f"{new_w}x{new_h}")
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
subprocess.run(cmd, capture_output=True, check=True, shell=False) # noqa: S603
|
# stdin=DEVNULL is critical: ffmpeg must not inherit the worker's
|
||||||
|
# stdin, which carries the framed request protocol. An inherited
|
||||||
|
# stdin lets ffmpeg eat protocol bytes and, if the worker is
|
||||||
|
# killed mid-conversion, keeps the orphaned ffmpeg holding the
|
||||||
|
# pipe open so the parent's proc.wait() hangs forever.
|
||||||
|
subprocess.run( # noqa: S603
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
check=True,
|
||||||
|
shell=False,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
shell_cmd = shlex.join(cmd)
|
shell_cmd = shlex.join(cmd)
|
||||||
stderr = (e.stderr or b"").decode(errors="replace").strip()
|
stderr = (e.stderr or b"").decode(errors="replace").strip()
|
||||||
@@ -540,11 +551,7 @@ def main() -> None:
|
|||||||
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
|
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
|
||||||
try:
|
try:
|
||||||
config.load_config()
|
config.load_config()
|
||||||
logger.warning(
|
logger.info("preview-worker config=%s", config.conffile)
|
||||||
"preview-worker config=%s master_secret=%s",
|
|
||||||
config.conffile,
|
|
||||||
config.config.secret,
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("preview-worker failed to load config at startup")
|
logger.exception("preview-worker failed to load config at startup")
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ def format_access_log(
|
|||||||
method: str,
|
method: str,
|
||||||
host: str,
|
host: str,
|
||||||
path: str,
|
path: str,
|
||||||
|
*,
|
||||||
duration_ms: float,
|
duration_ms: float,
|
||||||
extra: str | None = None,
|
extra: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
|
|||||||
@@ -6,9 +6,8 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "run-p type-check \"build-only {@}\" --",
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test:unit": "vitest",
|
|
||||||
"build-only": "vite build",
|
"build-only": "vite build",
|
||||||
"type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
|
||||||
"lint": "biome lint .",
|
"lint": "biome lint .",
|
||||||
"format": "biome format --write .",
|
"format": "biome format --write .",
|
||||||
"format:check": "biome format --check .",
|
"format:check": "biome format --check .",
|
||||||
@@ -37,17 +36,13 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.9.4",
|
"@biomejs/biome": "^1.9.4",
|
||||||
"@tsconfig/node18": "^18.2.6",
|
"@tsconfig/node18": "^18.2.6",
|
||||||
"@types/jsdom": "^27.0.0",
|
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^25.1.0",
|
"@types/node": "^25.1.0",
|
||||||
"@vitejs/plugin-vue": "^6.0.3",
|
"@vitejs/plugin-vue": "^6.0.3",
|
||||||
"@vue/test-utils": "^2.4.6",
|
|
||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
"jsdom": "^27.4.0",
|
|
||||||
"npm-run-all2": "^8.0.4",
|
"npm-run-all2": "^8.0.4",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vitest": "^4.0.18",
|
|
||||||
"vue-tsc": "^3.2.4"
|
"vue-tsc": "^3.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-15
@@ -58,13 +58,13 @@ import Router from '@/router/index'
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import AboutModal from './components/AboutModal.vue'
|
import AboutModal from './components/AboutModal.vue'
|
||||||
import AccessDeniedModal from './components/AccessDeniedModal.vue'
|
import AccessDeniedModal from './components/AccessDeniedModal.vue'
|
||||||
import ExplorerView from './views/ExplorerView.vue'
|
|
||||||
import SelectionToolbar from './components/SelectionToolbar.vue'
|
import SelectionToolbar from './components/SelectionToolbar.vue'
|
||||||
import TextEditorView from './views/TextEditorView.vue'
|
|
||||||
import type SettingsModalVue from './components/SettingsModal.vue'
|
import type SettingsModalVue from './components/SettingsModal.vue'
|
||||||
import UserManagementModal from './components/UserManagementModal.vue'
|
import UserManagementModal from './components/UserManagementModal.vue'
|
||||||
import UserTokensModal from './components/UserTokensModal.vue'
|
import UserTokensModal from './components/UserTokensModal.vue'
|
||||||
import type { SortOrder } from './utils/docsort'
|
import type { SortOrder } from './utils/docsort'
|
||||||
|
import ExplorerView from './views/ExplorerView.vue'
|
||||||
|
import TextEditorView from './views/TextEditorView.vue'
|
||||||
|
|
||||||
interface Path {
|
interface Path {
|
||||||
path: string
|
path: string
|
||||||
@@ -78,7 +78,9 @@ interface Path {
|
|||||||
const store = useMainStore()
|
const store = useMainStore()
|
||||||
|
|
||||||
const getDocByPath = (fullPath: string) =>
|
const getDocByPath = (fullPath: string) =>
|
||||||
getDocuments().find(doc => (doc.loc ? `${doc.loc}/${doc.name}` : doc.name) === fullPath)
|
getDocuments().find(
|
||||||
|
doc => (doc.loc ? `${doc.loc}/${doc.name}` : doc.name) === fullPath
|
||||||
|
)
|
||||||
|
|
||||||
const path: ComputedRef<Path> = computed(() => {
|
const path: ComputedRef<Path> = computed(() => {
|
||||||
const p = decodeURIComponent(Router.currentRoute.value.path).split('//')
|
const p = decodeURIComponent(Router.currentRoute.value.path).split('//')
|
||||||
@@ -90,13 +92,9 @@ const path: ComputedRef<Path> = computed(() => {
|
|||||||
void store.docVersion
|
void store.docVersion
|
||||||
const doc = fullPath ? getDocByPath(fullPath) : null
|
const doc = fullPath ? getDocByPath(fullPath) : null
|
||||||
const isEditorPath = !!(doc && !doc.dir && doc.text)
|
const isEditorPath = !!(doc && !doc.dir && doc.text)
|
||||||
const canonicalBase = !fullPath
|
const canonicalBase = !fullPath ? '/' : doc?.dir ? `/${fullPath}/` : `/${fullPath}`
|
||||||
? '/'
|
|
||||||
: doc?.dir
|
|
||||||
? `/${fullPath}/`
|
|
||||||
: `/${fullPath}`
|
|
||||||
const canonicalPath = query
|
const canonicalPath = query
|
||||||
? rawPath // keep search URL shape untouched
|
? `${rawPath}//${query}` // keep search URL shape untouched
|
||||||
: canonicalBase
|
: canonicalBase
|
||||||
const pathList = isEditorPath ? routePathList.slice(0, -1) : routePathList
|
const pathList = isEditorPath ? routePathList.slice(0, -1) : routePathList
|
||||||
const breadcrumbPathList = routePathList
|
const breadcrumbPathList = routePathList
|
||||||
@@ -128,14 +126,10 @@ const routeViewComponent = computed(() =>
|
|||||||
path.value.isEditorPath ? TextEditorView : ExplorerView
|
path.value.isEditorPath ? TextEditorView : ExplorerView
|
||||||
)
|
)
|
||||||
const routeViewKey = computed(() => {
|
const routeViewKey = computed(() => {
|
||||||
return path.value.isEditorPath
|
return path.value.isEditorPath ? `editor:${path.value.path}` : 'explorer'
|
||||||
? `editor:${path.value.path}`
|
|
||||||
: 'explorer'
|
|
||||||
})
|
})
|
||||||
const routeViewProps = computed(() =>
|
const routeViewProps = computed(() =>
|
||||||
path.value.isEditorPath
|
path.value.isEditorPath ? {} : { path: path.value.pathList, query: path.value.query }
|
||||||
? {}
|
|
||||||
: { path: path.value.pathList, query: path.value.query }
|
|
||||||
)
|
)
|
||||||
watch(
|
watch(
|
||||||
() => path.value.canonicalPath,
|
() => path.value.canonicalPath,
|
||||||
|
|||||||
@@ -122,8 +122,7 @@ watchEffect(() => {
|
|||||||
if (!same) {
|
if (!same) {
|
||||||
longest.value = props.path
|
longest.value = props.path
|
||||||
longestLinks.value = currentLinks
|
longestLinks.value = currentLinks
|
||||||
}
|
} else if (props.path.length > longcut.length) {
|
||||||
else if (props.path.length > longcut.length) {
|
|
||||||
longest.value = longcut.concat(props.path.slice(longcut.length))
|
longest.value = longcut.concat(props.path.slice(longcut.length))
|
||||||
longestLinks.value.splice(0, currentLinks.length, ...currentLinks)
|
longestLinks.value.splice(0, currentLinks.length, ...currentLinks)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -104,7 +104,9 @@ const showOtherCategory = computed(() => {
|
|||||||
return !!s.disk && otherBytes.value / s.disk >= 0.01
|
return !!s.disk && otherBytes.value / s.disk >= 0.01
|
||||||
})
|
})
|
||||||
const freeSliceBytes = computed(() =>
|
const freeSliceBytes = computed(() =>
|
||||||
showOtherCategory.value ? store.space.free : Math.max(0, store.space.disk - store.space.allocated)
|
showOtherCategory.value
|
||||||
|
? store.space.free
|
||||||
|
: Math.max(0, store.space.disk - store.space.allocated)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Calculate max label length based on angular gap to neighbor labels
|
// Calculate max label length based on angular gap to neighbor labels
|
||||||
@@ -295,7 +297,11 @@ const freeLabelPath = computed(() =>
|
|||||||
createArcPath(adjustedLabelAngles.value.free!, 'free', 4)
|
createArcPath(adjustedLabelAngles.value.free!, 'free', 4)
|
||||||
)
|
)
|
||||||
const otherLabelPath = computed(() =>
|
const otherLabelPath = computed(() =>
|
||||||
createArcPath(adjustedLabelAngles.value.other ?? sectorInfo.value.other.angle, 'other', 5)
|
createArcPath(
|
||||||
|
adjustedLabelAngles.value.other ?? sectorInfo.value.other.angle,
|
||||||
|
'other',
|
||||||
|
5
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleClick = () => (isExpanded.value ? collapse() : expand())
|
const handleClick = () => (isExpanded.value ? collapse() : expand())
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="!props.path || documents.length === 0" class="empty-container">
|
<div v-if="showEmpty" class="empty-container">
|
||||||
<component :is="cog" :class="['cog', { stopped: store.dialog === 'accessdenied' || store.authInProgress }]"/>
|
<component :is="cog" :class="['cog', { stopped: store.dialog === 'accessdenied' || store.authInProgress }]"/>
|
||||||
<p v-if="store.dialog === 'accessdenied'">Access Denied</p>
|
<p v-if="store.dialog === 'accessdenied'">Access Denied</p>
|
||||||
<p v-else-if="!store.connected">No Connection</p>
|
<p v-else-if="!store.connected">No Connection</p>
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
import { Cog } from '@/assets/svg'
|
import { Cog } from '@/assets/svg'
|
||||||
import { useMainStore } from '@/stores/main'
|
import { useMainStore } from '@/stores/main'
|
||||||
import { exists } from '@/utils/fileutil'
|
import { exists } from '@/utils/fileutil'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const cog = Cog
|
const cog = Cog
|
||||||
const store = useMainStore()
|
const store = useMainStore()
|
||||||
@@ -21,9 +22,29 @@ const props = defineProps<{
|
|||||||
path: string[]
|
path: string[]
|
||||||
documents: Document[]
|
documents: Document[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const showEmpty = computed(() => {
|
||||||
|
const loc = props.path.join('/')
|
||||||
|
const hasVisibleGhost = store.ghosts.some(g => {
|
||||||
|
const full = g.loc ? `${g.loc}/${g.name}` : g.name
|
||||||
|
return g.loc === loc && !store.hiddenPaths.has(full)
|
||||||
|
})
|
||||||
|
|
||||||
|
return !props.path || (props.documents.length === 0 && !hasVisibleGhost)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.empty-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 2rem;
|
||||||
|
text-shadow: 0 0 .3rem #000, 0 0 2rem #0008;
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
@keyframes rotate {
|
@keyframes rotate {
|
||||||
0% { transform: rotate(0deg); }
|
0% { transform: rotate(0deg); }
|
||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
|
|||||||
@@ -1,74 +1,77 @@
|
|||||||
<template>
|
<template>
|
||||||
<table v-if="props.documents.length || editing">
|
<div class="file-explorer">
|
||||||
<thead>
|
<table v-if="props.documents.length || editing">
|
||||||
<tr>
|
<thead>
|
||||||
<th class="selection">
|
<tr>
|
||||||
<input type="checkbox" tabindex="-1" v-model="allSelected" :indeterminate="selectionIndeterminate">
|
<th class="selection">
|
||||||
</th>
|
<input type="checkbox" tabindex="-1" v-model="allSelected" :indeterminate="selectionIndeterminate">
|
||||||
<th class="sortcolumn" :class="{ sortactive: store.sortOrder === 'name' }" @click="store.toggleSort('name')">Name</th>
|
</th>
|
||||||
<th class="sortcolumn modified right" :class="{ sortactive: store.sortOrder === 'modified' }" @click="store.toggleSort('modified')">Modified</th>
|
<th class="sortcolumn" :class="{ sortactive: store.sortOrder === 'name' }" @click="store.toggleSort('name')">Name</th>
|
||||||
<th class="sortcolumn size right" :class="{ sortactive: store.sortOrder === 'size' }" @click="store.toggleSort('size')">Size</th>
|
<th class="sortcolumn modified right" :class="{ sortactive: store.sortOrder === 'modified' }" @click="store.toggleSort('modified')">Modified</th>
|
||||||
<th class="menu"></th>
|
<th class="sortcolumn size right" :class="{ sortactive: store.sortOrder === 'size' }" @click="store.toggleSort('size')">Size</th>
|
||||||
</tr>
|
<th class="menu"></th>
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-if="editing?.key === 'new'" :class="editing.dir ? 'folder' : 'file'">
|
|
||||||
<td class="selection"></td>
|
|
||||||
<td class="name">
|
|
||||||
<FileRenameInput :doc="editing" :rename="createItem" :exit="() => {editing = null}" />
|
|
||||||
</td>
|
|
||||||
<FileModified :doc=editing :now=nowkey />
|
|
||||||
<FileSize :doc=editing />
|
|
||||||
<td class="menu"></td>
|
|
||||||
</tr>
|
|
||||||
<template v-for="(doc, index) in documents" :key="doc.key">
|
|
||||||
<tr class="folder-change" v-if="showFolderBreadcrumb(index)">
|
|
||||||
<th colspan="5"><BreadCrumb :path="doc.loc ? doc.loc.split('/') : []" /></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
|
</thead>
|
||||||
<tr
|
<tbody>
|
||||||
:id="`file-${doc.key}`"
|
<tr v-if="editing?.key === 'new'" :class="editing.dir ? 'folder' : 'file'">
|
||||||
:class="{ file: !doc.dir, folder: doc.dir, cursor: store.cursor === doc.key, ghost: doc.ghost }"
|
<td class="selection"></td>
|
||||||
@click="store.cursor = store.cursor === doc.key ? '' : doc.key"
|
|
||||||
@contextmenu.prevent="contextMenu($event, doc)"
|
|
||||||
>
|
|
||||||
<td class="selection" @click.up.stop="store.cursor = store.cursor === doc.key ? doc.key : ''">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
tabindex="-1"
|
|
||||||
:checked="store.selected.has(doc.key)"
|
|
||||||
@change="
|
|
||||||
($event.target as HTMLInputElement).checked
|
|
||||||
? store.selected.add(doc.key)
|
|
||||||
: store.selected.delete(doc.key)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td class="name">
|
<td class="name">
|
||||||
<template v-if="editing === doc">
|
<FileRenameInput :doc="editing" :rename="createItem" :exit="exitEditing" />
|
||||||
<FileRenameInput :doc="doc" :rename="rename" :exit="() => {editing = null}" />
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<a :href="doc.text ? doc.editurl : doc.url" tabindex=-1 @contextmenu.stop @focus.stop="store.cursor = doc.key">
|
|
||||||
{{ doc.name }}
|
|
||||||
</a>
|
|
||||||
<button tabindex=-1 v-if="store.cursor == doc.key" class="rename-button" @click="() => (editing = doc)">🖊️</button>
|
|
||||||
</template>
|
|
||||||
</td>
|
|
||||||
<FileModified :doc=doc :now=nowkey />
|
|
||||||
<FileSize :doc=doc />
|
|
||||||
<td class="menu">
|
|
||||||
<button tabindex=-1 @click.stop="contextMenu($event, doc)">⋮</button>
|
|
||||||
</td>
|
</td>
|
||||||
|
<FileModified :doc=editing :now=nowkey />
|
||||||
|
<FileSize :doc=editing />
|
||||||
|
<td class="menu"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
<template v-for="(doc, index) in documents" :key="doc.key">
|
||||||
<tr class="summary" v-if="props.documents.length > 1">
|
<tr class="folder-change" v-if="showFolderBreadcrumb(index)">
|
||||||
<td colspan="3" class="right">{{props.documents.length}} items</td>
|
<th colspan="5"><BreadCrumb :path="doc.loc ? doc.loc.split('/') : []" /></th>
|
||||||
<td class="size right">{{ formatSize(props.documents.reduce((a, b) => a + b.size, 0)) }}</td>
|
</tr>
|
||||||
<td class="menu"></td>
|
|
||||||
</tr>
|
<tr
|
||||||
</tbody>
|
:id="`file-${doc.key}`"
|
||||||
</table>
|
:class="{ file: !doc.dir, folder: doc.dir, cursor: store.cursor === doc.key, ghost: doc.ghost }"
|
||||||
|
@click="store.cursor = store.cursor === doc.key ? '' : doc.key"
|
||||||
|
@contextmenu.prevent="contextMenu($event, doc)"
|
||||||
|
>
|
||||||
|
<td class="selection" @click.up.stop="store.cursor = store.cursor === doc.key ? doc.key : ''">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
tabindex="-1"
|
||||||
|
:checked="store.selected.has(doc.key)"
|
||||||
|
@change="
|
||||||
|
($event.target as HTMLInputElement).checked
|
||||||
|
? store.selected.add(doc.key)
|
||||||
|
: store.selected.delete(doc.key)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class="name">
|
||||||
|
<template v-if="editing === doc">
|
||||||
|
<FileRenameInput :doc="doc" :rename="rename" :exit="exitEditing" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a :href="doc.text ? doc.editurl : doc.url" tabindex=-1 @contextmenu.stop @focus.stop="store.cursor = doc.key">
|
||||||
|
{{ doc.name }}
|
||||||
|
</a>
|
||||||
|
<button tabindex=-1 class="rename-button" @click="() => (editing = doc)">🖊️</button>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
<FileModified :doc=doc :now=nowkey />
|
||||||
|
<FileSize :doc=doc />
|
||||||
|
<td class="menu">
|
||||||
|
<button tabindex=-1 @click.stop="contextMenu($event, doc)">⋮</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<tr class="summary" v-if="props.documents.length > 1">
|
||||||
|
<td colspan="3" class="right">{{props.documents.length}} items</td>
|
||||||
|
<td class="size right">{{ formatSize(props.documents.reduce((a, b) => a + b.size, 0)) }}</td>
|
||||||
|
<td class="menu"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<EmptyFolder v-else :documents="documents" :path="props.path" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -81,11 +84,13 @@ import ContextMenu from '@imengyu/vue3-context-menu'
|
|||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
nextTick,
|
nextTick,
|
||||||
|
onActivated,
|
||||||
|
onDeactivated,
|
||||||
onMounted,
|
onMounted,
|
||||||
onUnmounted,
|
onUnmounted,
|
||||||
ref,
|
ref,
|
||||||
shallowRef,
|
shallowRef,
|
||||||
watchEffect
|
watch
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import FileRenameInput from './FileRenameInput.vue'
|
import FileRenameInput from './FileRenameInput.vue'
|
||||||
@@ -189,6 +194,9 @@ const pageMove = (direction: 1 | -1, ev: KeyboardEvent) => {
|
|||||||
|
|
||||||
// File rename
|
// File rename
|
||||||
const editing = shallowRef<Doc | null>(null)
|
const editing = shallowRef<Doc | null>(null)
|
||||||
|
const exitEditing = () => {
|
||||||
|
editing.value = null
|
||||||
|
}
|
||||||
const rename = async (doc: Doc, newName: string) => {
|
const rename = async (doc: Doc, newName: string) => {
|
||||||
const oldName = doc.name
|
const oldName = doc.name
|
||||||
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
|
||||||
@@ -243,7 +251,7 @@ defineExpose({
|
|||||||
const docs = props.documents
|
const docs = props.documents
|
||||||
if (docs.length > 0) {
|
if (docs.length > 0) {
|
||||||
store.cursor = docs[0]!.key
|
store.cursor = docs[0]!.key
|
||||||
// Also focus the element directly (watchEffect won't trigger if cursor unchanged)
|
// Also focus the element directly (post-flush watcher won't trigger if cursor unchanged)
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const a = document.querySelector(
|
const a = document.querySelector(
|
||||||
`#file-${store.cursor} .name a`
|
`#file-${store.cursor} .name a`
|
||||||
@@ -329,22 +337,41 @@ const focusBreadcrumb = () => {
|
|||||||
const keyboardFollowScroll = createKeyboardFollowScroll()
|
const keyboardFollowScroll = createKeyboardFollowScroll()
|
||||||
const markKeyboardFollow = keyboardFollowScroll.markKeyboardFollow
|
const markKeyboardFollow = keyboardFollowScroll.markKeyboardFollow
|
||||||
const keepCursorVisibleSmooth = keyboardFollowScroll.keepVisible
|
const keepCursorVisibleSmooth = keyboardFollowScroll.keepVisible
|
||||||
watchEffect(() => {
|
// Deactivated (KeepAlive-cached) instances stay alive with frozen, potentially
|
||||||
if (store.cursor && store.cursor !== editing.value?.key) editing.value = null
|
// stale props - their watchers must not react to global store changes.
|
||||||
if (editing.value) store.cursor = editing.value?.key
|
let isActive = true
|
||||||
if (store.cursor) {
|
watch(
|
||||||
const a = document.querySelector(
|
() => store.cursor,
|
||||||
`#file-${store.cursor} .name a`
|
cursor => {
|
||||||
) as HTMLAnchorElement | null
|
if (!isActive) return
|
||||||
if (a) a.focus({ preventScroll: true })
|
if (cursor && editing.value && cursor !== editing.value.key) {
|
||||||
|
exitEditing()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
watchEffect(() => {
|
watch(
|
||||||
if (!props.documents.length && store.cursor && !store.query) {
|
() => store.cursor,
|
||||||
store.cursor = ''
|
cursor => {
|
||||||
focusBreadcrumb()
|
if (!isActive) return
|
||||||
|
if (cursor && !editing.value) {
|
||||||
|
const a = document.querySelector(
|
||||||
|
`#file-${cursor} .name a`
|
||||||
|
) as HTMLAnchorElement | null
|
||||||
|
if (a) a.focus({ preventScroll: true })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ flush: 'post' }
|
||||||
|
)
|
||||||
|
watch(
|
||||||
|
() => [props.documents.length, store.cursor, store.query, editing.value] as const,
|
||||||
|
([len, cursor, query, editingDoc]) => {
|
||||||
|
if (!isActive) return
|
||||||
|
if (!len && cursor && !query && !editingDoc) {
|
||||||
|
store.cursor = ''
|
||||||
|
focusBreadcrumb()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
let nowkey = ref(0)
|
let nowkey = ref(0)
|
||||||
let modifiedTimer: any = null
|
let modifiedTimer: any = null
|
||||||
const updateModified = () => {
|
const updateModified = () => {
|
||||||
@@ -358,6 +385,13 @@ onMounted(() => {
|
|||||||
active.focus({ preventScroll: true })
|
active.focus({ preventScroll: true })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
onActivated(() => {
|
||||||
|
isActive = true
|
||||||
|
})
|
||||||
|
onDeactivated(() => {
|
||||||
|
isActive = false
|
||||||
|
if (editing.value) exitEditing()
|
||||||
|
})
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
keyboardFollowScroll.cancel()
|
keyboardFollowScroll.cancel()
|
||||||
clearInterval(modifiedTimer)
|
clearInterval(modifiedTimer)
|
||||||
@@ -373,7 +407,8 @@ const createItem = async (doc: Doc, name: string) => {
|
|||||||
doc.name = name
|
doc.name = name
|
||||||
doc.key = crypto.randomUUID()
|
doc.key = crypto.randomUUID()
|
||||||
store.addGhost(doc)
|
store.addGhost(doc)
|
||||||
editing.value = null
|
store.cursor = doc.key
|
||||||
|
exitEditing()
|
||||||
const path = doc.loc ? `${doc.loc}/${name}` : name
|
const path = doc.loc ? `${doc.loc}/${name}` : name
|
||||||
try {
|
try {
|
||||||
const res = doc.dir
|
const res = doc.dir
|
||||||
@@ -525,9 +560,14 @@ const contextMenu = (ev: MouseEvent, doc: Doc) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.file-explorer {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
thead tr {
|
thead tr {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
@@ -588,6 +628,12 @@ table td {
|
|||||||
.name .rename-button {
|
.name .rename-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
tbody tr:hover .name .rename-button {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
animation: appear calc(5 * var(--transition-time)) linear;
|
animation: appear calc(5 * var(--transition-time)) linear;
|
||||||
}
|
}
|
||||||
@keyframes appear {
|
@keyframes appear {
|
||||||
@@ -658,12 +704,6 @@ tbody .selection input {
|
|||||||
content: '📁';
|
content: '📁';
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
.empty-container {
|
|
||||||
padding-top: 3rem;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 3rem;
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
||||||
.folder-change {
|
.folder-change {
|
||||||
margin-left: -.5rem;
|
margin-left: -.5rem;
|
||||||
}
|
}
|
||||||
@@ -674,4 +714,3 @@ tbody .selection input {
|
|||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@/stores/main
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ input#FileRenameInput {
|
|||||||
padding: .75em;
|
padding: .75em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,11 +8,12 @@
|
|||||||
:editing="editing === doc ? {rename, exit} : null"
|
:editing="editing === doc ? {rename, exit} : null"
|
||||||
:style="{ '--gallery-figure-height': rowHeightsByKey[doc.key] ?? '15em' }"
|
:style="{ '--gallery-figure-height': rowHeightsByKey[doc.key] ?? '15em' }"
|
||||||
@menu="contextMenu($event, doc)"
|
@menu="contextMenu($event, doc)"
|
||||||
@rename="editing = doc; store.cursor = doc.key"
|
@rename="onFigureRename(doc)"
|
||||||
:class="{ 'folder-start': showFolderBreadcrumb(index) }"
|
:class="{ 'folder-start': showFolderBreadcrumb(index) }"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<EmptyFolder v-else :documents="documents" :path="props.path" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -23,16 +24,15 @@ import type { SortOrder } from '@/utils/docsort'
|
|||||||
import { createKeyboardFollowScroll } from '@/utils/keyboardFollowScroll'
|
import { createKeyboardFollowScroll } from '@/utils/keyboardFollowScroll'
|
||||||
import ContextMenu from '@imengyu/vue3-context-menu'
|
import ContextMenu from '@imengyu/vue3-context-menu'
|
||||||
import {
|
import {
|
||||||
onActivated,
|
|
||||||
onDeactivated,
|
|
||||||
computed,
|
computed,
|
||||||
nextTick,
|
nextTick,
|
||||||
|
onActivated,
|
||||||
|
onDeactivated,
|
||||||
onMounted,
|
onMounted,
|
||||||
onUnmounted,
|
onUnmounted,
|
||||||
ref,
|
ref,
|
||||||
shallowRef,
|
shallowRef,
|
||||||
watch,
|
watch
|
||||||
watchEffect
|
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
@@ -64,6 +64,10 @@ const editing = shallowRef<Doc | null>(null)
|
|||||||
const exit = () => {
|
const exit = () => {
|
||||||
editing.value = null
|
editing.value = null
|
||||||
}
|
}
|
||||||
|
const onFigureRename = (doc: Doc) => {
|
||||||
|
editing.value = doc
|
||||||
|
store.cursor = doc.key
|
||||||
|
}
|
||||||
const rename = async (doc: Doc, newName: string) => {
|
const rename = async (doc: Doc, newName: string) => {
|
||||||
const oldName = doc.name
|
const oldName = doc.name
|
||||||
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
|
||||||
@@ -304,7 +308,7 @@ defineExpose({
|
|||||||
const docs = props.documents
|
const docs = props.documents
|
||||||
if (docs.length > 0) {
|
if (docs.length > 0) {
|
||||||
store.cursor = docs[0]!.key
|
store.cursor = docs[0]!.key
|
||||||
// Also focus the element directly (watchEffect won't trigger if cursor unchanged)
|
// Also focus the element directly (post-flush watcher won't trigger if cursor unchanged)
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const a = document.querySelector(
|
const a = document.querySelector(
|
||||||
`#file-${store.cursor}`
|
`#file-${store.cursor}`
|
||||||
@@ -396,24 +400,41 @@ const focusBreadcrumb = () => {
|
|||||||
const keyboardFollowScroll = createKeyboardFollowScroll()
|
const keyboardFollowScroll = createKeyboardFollowScroll()
|
||||||
const markKeyboardFollow = keyboardFollowScroll.markKeyboardFollow
|
const markKeyboardFollow = keyboardFollowScroll.markKeyboardFollow
|
||||||
const keepCursorVisibleSmooth = keyboardFollowScroll.keepVisible
|
const keepCursorVisibleSmooth = keyboardFollowScroll.keepVisible
|
||||||
watchEffect(() => {
|
// Deactivated (KeepAlive-cached) instances stay alive with frozen, potentially
|
||||||
if (store.cursor && store.cursor !== editing.value?.key) editing.value = null
|
// stale props - their watchers must not react to global store changes.
|
||||||
if (editing.value) store.cursor = editing.value.key
|
let isActive = true
|
||||||
if (store.cursor && !editing.value) {
|
watch(
|
||||||
const a = document.querySelector(
|
() => store.cursor,
|
||||||
`#file-${store.cursor}`
|
cursor => {
|
||||||
) as HTMLAnchorElement | null
|
if (!isActive) return
|
||||||
if (a) {
|
if (cursor && editing.value && cursor !== editing.value.key) {
|
||||||
a.focus({ preventScroll: true })
|
exit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
watchEffect(() => {
|
watch(
|
||||||
if (!props.documents.length && store.cursor && !store.query) {
|
() => store.cursor,
|
||||||
store.cursor = ''
|
cursor => {
|
||||||
focusBreadcrumb()
|
if (!isActive) return
|
||||||
|
if (cursor && !editing.value) {
|
||||||
|
const a = document.querySelector(`#file-${cursor}`) as HTMLAnchorElement | null
|
||||||
|
if (a) {
|
||||||
|
a.focus({ preventScroll: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ flush: 'post' }
|
||||||
|
)
|
||||||
|
watch(
|
||||||
|
() => [props.documents.length, store.cursor, store.query, editing.value] as const,
|
||||||
|
([len, cursor, query, editingDoc]) => {
|
||||||
|
if (!isActive) return
|
||||||
|
if (!len && cursor && !query && !editingDoc) {
|
||||||
|
store.cursor = ''
|
||||||
|
focusBreadcrumb()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
let resizeObserver: ResizeObserver | null = null
|
let resizeObserver: ResizeObserver | null = null
|
||||||
const attachGalleryObservers = () => {
|
const attachGalleryObservers = () => {
|
||||||
if (!gallery.value || resizeObserver) return
|
if (!gallery.value || resizeObserver) return
|
||||||
@@ -438,13 +459,16 @@ onMounted(() => {
|
|||||||
attachGalleryObservers()
|
attachGalleryObservers()
|
||||||
})
|
})
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
|
isActive = true
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
updateColumns()
|
updateColumns()
|
||||||
attachGalleryObservers()
|
attachGalleryObservers()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
onDeactivated(() => {
|
onDeactivated(() => {
|
||||||
|
isActive = false
|
||||||
detachGalleryObservers()
|
detachGalleryObservers()
|
||||||
|
if (editing.value) exit()
|
||||||
})
|
})
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
keyboardFollowScroll.cancel()
|
keyboardFollowScroll.cancel()
|
||||||
@@ -464,7 +488,8 @@ const createItem = async (doc: Doc, name: string) => {
|
|||||||
doc.name = name
|
doc.name = name
|
||||||
doc.key = crypto.randomUUID()
|
doc.key = crypto.randomUUID()
|
||||||
store.addGhost(doc)
|
store.addGhost(doc)
|
||||||
editing.value = null
|
store.cursor = doc.key
|
||||||
|
exit()
|
||||||
const path = doc.loc ? `${doc.loc}/${name}` : name
|
const path = doc.loc ? `${doc.loc}/${name}` : name
|
||||||
try {
|
try {
|
||||||
const res = doc.dir
|
const res = doc.dir
|
||||||
@@ -612,7 +637,8 @@ const contextMenu = (ev: MouseEvent, doc: Doc) => {
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: .5em;
|
gap: .5em;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(15em, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(15em, 1fr));
|
||||||
align-items: end;
|
align-items: start;
|
||||||
|
align-content: start;
|
||||||
}
|
}
|
||||||
.folder-indicator {
|
.folder-indicator {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<span class="filename">{{ snap.displayName }}<SparseIndicator :doc="doc" class="after-name" /></span>
|
<span class="filename">{{ snap.displayName }}<SparseIndicator :doc="doc" class="after-name" /></span>
|
||||||
<span v-if="snap.ext" class="file-ext">.{{ snap.ext }}</span>
|
<span v-if="snap.ext" class="file-ext">.{{ snap.ext }}</span>
|
||||||
</span>
|
</span>
|
||||||
<button class="rename-btn" @click="$emit('rename')" title="Rename">✏️</button>
|
<button class="rename-btn" @click="emit('rename')" title="Rename">✏️</button>
|
||||||
</div>
|
</div>
|
||||||
<div class=namespacer></div>
|
<div class=namespacer></div>
|
||||||
</template>
|
</template>
|
||||||
@@ -64,6 +64,10 @@ const props = defineProps<{
|
|||||||
doc: Doc
|
doc: Doc
|
||||||
editing?: EditingProp
|
editing?: EditingProp
|
||||||
}>()
|
}>()
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'rename'): void
|
||||||
|
(e: 'menu', ev: MouseEvent): void
|
||||||
|
}>()
|
||||||
const m = ref<typeof MediaPreview | null>(null)
|
const m = ref<typeof MediaPreview | null>(null)
|
||||||
const tooltip = ref<InstanceType<typeof CursorTooltip> | null>(null)
|
const tooltip = ref<InstanceType<typeof CursorTooltip> | null>(null)
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,16 @@ export const exists = (path: string[]) => {
|
|||||||
void store.docVersion
|
void store.docVersion
|
||||||
if (path.length === 0) return true
|
if (path.length === 0) return true
|
||||||
const p = path.join('/')
|
const p = path.join('/')
|
||||||
return getDocuments().some(
|
const hidden = store.hiddenPaths
|
||||||
doc => (doc.loc ? `${doc.loc}/${doc.name}` : doc.name) === p
|
const inDocs = getDocuments().some(doc => {
|
||||||
)
|
const full = doc.loc ? `${doc.loc}/${doc.name}` : doc.name
|
||||||
|
return full === p && !hidden.has(full)
|
||||||
|
})
|
||||||
|
if (inDocs) return true
|
||||||
|
return store.ghosts.some(g => {
|
||||||
|
const full = g.loc ? `${g.loc}/${g.name}` : g.name
|
||||||
|
return full === p && !hidden.has(full)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Strip file extension intelligently (handles .tar.gz, name.with.dots.pdf, etc.) */
|
/** Strip file extension intelligently (handles .tar.gz, name.with.dots.pdf, etc.) */
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
/>
|
/>
|
||||||
</KeepAlive>
|
</KeepAlive>
|
||||||
</Transition>
|
</Transition>
|
||||||
<EmptyFolder :documents="documents" :path="props.path" />
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="store.searchLoading" class="search-loading">Searching...</div>
|
<div v-if="store.searchLoading" class="search-loading">Searching...</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -38,7 +37,9 @@ const props = defineProps<{
|
|||||||
|
|
||||||
// Folder path for component keys - only recreate component when folder changes, not search
|
// Folder path for component keys - only recreate component when folder changes, not search
|
||||||
const folderPath = computed(() => props.path.join('/'))
|
const folderPath = computed(() => props.path.join('/'))
|
||||||
const cacheKey = computed(() => `${store.prefs.gallery ? 'gallery' : 'list'}:${folderPath.value}`)
|
const cacheKey = computed(
|
||||||
|
() => `${store.prefs.gallery ? 'gallery' : 'list'}:${folderPath.value}`
|
||||||
|
)
|
||||||
|
|
||||||
const transitionName = computed(() => {
|
const transitionName = computed(() => {
|
||||||
if (store.transitionDirection === 'forward') return 'slide-forward'
|
if (store.transitionDirection === 'forward') return 'slide-forward'
|
||||||
@@ -140,16 +141,6 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.empty-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 2rem;
|
|
||||||
text-shadow: 0 0 .3rem #000, 0 0 2rem #0008;
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
||||||
.search-loading {
|
.search-loading {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 1rem;
|
bottom: 1rem;
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { apiFetch } from '@/repositories/Client'
|
||||||
|
import { useMainStore } from '@/stores/main'
|
||||||
import { indentWithTab } from '@codemirror/commands'
|
import { indentWithTab } from '@codemirror/commands'
|
||||||
import { LanguageDescription } from '@codemirror/language'
|
import { LanguageDescription } from '@codemirror/language'
|
||||||
import { languages } from '@codemirror/language-data'
|
import { languages } from '@codemirror/language-data'
|
||||||
@@ -16,8 +18,6 @@ import { Compartment, EditorState } from '@codemirror/state'
|
|||||||
import { oneDark } from '@codemirror/theme-one-dark'
|
import { oneDark } from '@codemirror/theme-one-dark'
|
||||||
import { EditorView, keymap } from '@codemirror/view'
|
import { EditorView, keymap } from '@codemirror/view'
|
||||||
import { basicSetup } from 'codemirror'
|
import { basicSetup } from 'codemirror'
|
||||||
import { apiFetch } from '@/repositories/Client'
|
|
||||||
import { useMainStore } from '@/stores/main'
|
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
nextTick,
|
nextTick,
|
||||||
@@ -173,8 +173,7 @@ onMounted(async () => {
|
|||||||
await initEditor(text)
|
await initEditor(text)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err instanceof Error ? err.message : 'Failed to load file'
|
error.value = err instanceof Error ? err.message : 'Failed to load file'
|
||||||
}
|
} finally {
|
||||||
finally {
|
|
||||||
if (loading.value) loading.value = false
|
if (loading.value) loading.value = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,9 +6,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "./tsconfig.app.json"
|
"path": "./tsconfig.app.json"
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.vitest.json"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": "@tsconfig/node18/tsconfig.json",
|
"extends": "@tsconfig/node18/tsconfig.json",
|
||||||
"include": [
|
"include": ["vite.config.*"],
|
||||||
"vite.config.*",
|
|
||||||
"vitest.config.*",
|
|
||||||
"cypress.config.*",
|
|
||||||
"nightwatch.conf.*",
|
|
||||||
"playwright.config.*"
|
|
||||||
],
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.app.json",
|
|
||||||
"exclude": [],
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"types": ["node", "jsdom"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -132,6 +132,7 @@ ignore = [
|
|||||||
"ANN205", # legacy codebase: no full runtime annotation coverage yet
|
"ANN205", # legacy codebase: no full runtime annotation coverage yet
|
||||||
"BLE001", # broad catch remains in boundary/proxy/error-handling paths
|
"BLE001", # broad catch remains in boundary/proxy/error-handling paths
|
||||||
"C901", # legacy complexity; keep other correctness rules enabled
|
"C901", # legacy complexity; keep other correctness rules enabled
|
||||||
|
"CPY", # copyright notices not wanted in this codebase
|
||||||
"D100", # legacy docs not yet standardized
|
"D100", # legacy docs not yet standardized
|
||||||
"D101", # legacy docs not yet standardized
|
"D101", # legacy docs not yet standardized
|
||||||
"D102", # legacy docs not yet standardized
|
"D102", # legacy docs not yet standardized
|
||||||
|
|||||||
Reference in New Issue
Block a user