refactor(frontend): extract shared modules, thin views, delete dead code

Shared utilities:
- Add src/utils/formatters.js — formatDate, formatSize, providerColor,
  providerBg, providerLabel; all components import from here, no inline duplicates
- Add src/components/ui/TreeItem.vue — generic expand/collapse tree node;
  FolderTreeItem, CloudFolderTreeItem, CloudProviderTreeItem now wrap it
- Add src/components/storage/StorageBrowser.vue — unified file browser grid
  used by both FileManagerView and CloudFolderView

View refactor (thin data-providers):
- FileManagerView.vue: stripped to props + event wiring; all layout moved to StorageBrowser
- CloudFolderView.vue: same treatment — feeds props into StorageBrowser
- All tree sidebar components delegate expand/collapse to TreeItem.vue

Dead code removed:
- Delete HomeView.vue — no active route, replaced by FileManagerView
- Delete FolderView.vue — no active route, logic merged into FileManagerView

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-06-02 16:10:47 +02:00
co-authored by Claude Sonnet 4.6
parent a548266461
commit cce70b2ef6
15 changed files with 728 additions and 1087 deletions
@@ -1,72 +1,34 @@
<template>
<div>
<!-- Row -->
<div
class="flex items-center group"
:style="{ paddingLeft: `${depth * 12}px` }"
>
<!-- Expand/collapse arrow (only for directories) -->
<button
<TreeItem
:label="folder.name"
:expandable="folder.is_dir"
:load-children="loadChildren"
:depth="depth"
@select="navigate"
>
<template #icon>
<svg
v-if="folder.is_dir"
@click.prevent.stop="toggleExpand"
class="w-5 h-5 flex items-center justify-center text-gray-400 hover:text-gray-600 shrink-0 transition-colors"
:aria-label="expanded ? 'Collapse ' + folder.name : 'Expand ' + folder.name"
class="w-4 h-4 shrink-0 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<svg
class="w-3 h-3 transition-transform duration-150"
:class="{ 'rotate-90': expanded }"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 5l7 7-7 7" />
</svg>
</button>
<!-- Spacer for non-directory items -->
<span v-else class="w-5 h-5 shrink-0"></span>
<!-- Folder/file name button -->
<button
@click="navigateTo"
class="flex items-center gap-2 flex-1 min-w-0 px-2 py-1.5 rounded-lg text-sm font-medium transition-colors text-gray-600 hover:bg-gray-100 hover:text-gray-900"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" />
</svg>
<svg
v-else
class="w-4 h-4 shrink-0 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<!-- Folder icon for directories, document icon for files -->
<svg
v-if="folder.is_dir"
class="w-4 h-4 shrink-0 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" />
</svg>
<svg
v-else
class="w-4 h-4 shrink-0 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
<span class="truncate">{{ folder.name }}</span>
</button>
</div>
<!-- Children: nested sub-folders (lazy loaded) -->
<template v-if="expanded">
<div v-if="loading" class="text-xs text-gray-400 py-1" :style="{ paddingLeft: `${(depth + 1) * 12 + 8}px` }">Loading</div>
<div
v-else-if="loadError"
class="text-xs text-red-500 cursor-pointer py-1"
:style="{ paddingLeft: `${(depth + 1) * 12 + 8}px` }"
@click="retry"
>
Failed to load tap to retry
</div>
<div v-else-if="children.length === 0" class="text-xs text-gray-400 py-1" :style="{ paddingLeft: `${(depth + 1) * 12 + 8}px` }">Empty</div>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
</template>
<template #children="{ children }">
<CloudFolderTreeItem
v-for="child in children"
:key="child.id"
@@ -75,13 +37,13 @@
:depth="depth + 1"
/>
</template>
</div>
</TreeItem>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import * as api from '../../api/client.js'
import TreeItem from '../ui/TreeItem.vue'
const props = defineProps({
folder: { type: Object, required: true },
@@ -91,38 +53,12 @@ const props = defineProps({
const router = useRouter()
const expanded = ref(false)
const children = ref([])
const loading = ref(false)
const loadError = ref(false)
const childrenLoaded = ref(false)
async function loadChildren() {
loading.value = true
loadError.value = false
try {
const data = await api.getCloudFolders(props.provider, props.folder.id)
children.value = (data.items ?? []).filter(i => i.is_dir)
childrenLoaded.value = true
} catch {
loadError.value = true
} finally {
loading.value = false
}
const data = await api.getCloudFolders(props.provider, props.folder.id)
return (data.items ?? []).filter(i => i.is_dir)
}
async function toggleExpand() {
if (!expanded.value && !childrenLoaded.value) {
await loadChildren()
}
expanded.value = !expanded.value
}
async function retry() {
await loadChildren()
}
function navigateTo() {
function navigate() {
router.push(`/cloud/${props.provider}/${props.folder.id}`)
}
</script>
@@ -1,52 +1,17 @@
<template>
<div>
<!-- Row -->
<div
class="flex items-center group"
:style="{ paddingLeft: `${depth * 12}px` }"
>
<!-- Expand/collapse arrow -->
<button
@click.prevent.stop="toggleExpand"
class="w-5 h-5 flex items-center justify-center text-gray-400 hover:text-gray-600 shrink-0 transition-colors"
:aria-label="expanded ? 'Collapse ' + connection.display_name : 'Expand ' + connection.display_name"
>
<svg
class="w-3 h-3 transition-transform duration-150"
:class="{ 'rotate-90': expanded }"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 5l7 7-7 7" />
</svg>
</button>
<!-- Provider name (click navigates to /settings) -->
<button
@click="navigateToRoot"
class="flex items-center gap-2 flex-1 min-w-0 px-2 py-1.5 rounded-lg text-sm font-medium transition-colors text-gray-600 hover:bg-gray-100 hover:text-gray-900"
>
<!-- Provider cloud icon (w-4 h-4, provider color) -->
<svg class="w-4 h-4 shrink-0" :class="providerIconColor" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
</svg>
<span class="truncate">{{ connection.display_name }}</span>
</button>
</div>
<!-- Children: first-level cloud folders (lazy loaded) -->
<template v-if="expanded">
<div v-if="loading" class="pl-12 py-1 text-xs text-gray-400">Loading</div>
<div
v-else-if="loadError"
class="pl-12 py-1 text-xs text-red-500 cursor-pointer"
@click="retry"
>
Failed to load tap to retry
</div>
<div v-else-if="children.length === 0" class="pl-12 py-1 text-xs text-gray-400">Empty</div>
<TreeItem
:label="connection.display_name"
:load-children="loadChildren"
:depth="depth"
@select="navigateToRoot"
>
<template #icon>
<svg class="w-4 h-4 shrink-0" :class="providerIconColor" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
</svg>
</template>
<template #children="{ children }">
<CloudFolderTreeItem
v-for="folder in children"
:key="folder.id"
@@ -55,14 +20,16 @@
:depth="depth + 1"
/>
</template>
</div>
</TreeItem>
</template>
<script setup>
import { ref, computed } from 'vue'
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import * as api from '../../api/client.js'
import TreeItem from '../ui/TreeItem.vue'
import CloudFolderTreeItem from './CloudFolderTreeItem.vue'
import { providerColor } from '../../utils/formatters.js'
const props = defineProps({
connection: { type: Object, required: true },
@@ -71,45 +38,11 @@ const props = defineProps({
const router = useRouter()
const expanded = ref(false)
const children = ref([])
const loading = ref(false)
const loadError = ref(false)
const childrenLoaded = ref(false)
const providerIconColor = computed(() => {
const map = {
google_drive: 'text-blue-500',
onedrive: 'text-sky-500',
nextcloud: 'text-orange-500',
webdav: 'text-gray-500',
}
return map[props.connection.provider] ?? 'text-gray-400'
})
const providerIconColor = computed(() => providerColor(props.connection.provider))
async function loadChildren() {
loading.value = true
loadError.value = false
try {
const data = await api.getCloudFolders(props.connection.provider, 'root')
children.value = (data.items ?? []).filter(i => i.is_dir)
childrenLoaded.value = true
} catch {
loadError.value = true
} finally {
loading.value = false
}
}
async function toggleExpand() {
if (!expanded.value && !childrenLoaded.value) {
await loadChildren()
}
expanded.value = !expanded.value
}
async function retry() {
await loadChildren()
const data = await api.getCloudFolders(props.connection.provider, 'root')
return (data.items ?? []).filter(i => i.is_dir)
}
function navigateToRoot() {