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
+134
View File
@@ -0,0 +1,134 @@
<template>
<div>
<!-- Row -->
<div
class="flex items-center group"
:style="{ paddingLeft: `${depth * 12}px` }"
>
<!-- Expand/collapse arrow hidden when item is a known leaf -->
<button
v-if="expandable"
@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' : 'Expand'"
>
<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>
<span v-else class="w-5 h-5 shrink-0"></span>
<!-- Label row router-link when `to` is provided, button otherwise -->
<router-link
v-if="to"
:to="to"
class="flex items-center gap-2 flex-1 min-w-0 px-2 py-1.5 rounded-lg text-sm font-medium transition-colors"
:class="isActive
? 'bg-indigo-50 text-indigo-700'
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'"
>
<slot name="icon" />
<span class="truncate">{{ label }}</span>
</router-link>
<button
v-else
@click="$emit('select')"
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"
>
<slot name="icon" />
<span class="truncate">{{ label }}</span>
</button>
</div>
<!-- Children -->
<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>
<slot name="children" :children="children" />
</template>
</div>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps({
label: { type: String, required: true },
/** Router-link destination. When omitted, the label row renders as a button that emits 'select'. */
to: { type: [String, Object], default: null },
/** Whether this item can be expanded to reveal children. */
expandable: { type: Boolean, default: true },
/** Async function that returns an array of child items when called. */
loadChildren: { type: Function, required: true },
depth: { type: Number, default: 0 },
/** Highlight the label row as the active route item. */
isActive: { type: Boolean, default: false },
})
const emit = defineEmits(['select'])
const expanded = ref(false)
const children = ref([])
const loading = ref(false)
const loadError = ref(false)
const childrenLoaded = ref(false)
async function load() {
loading.value = true
loadError.value = false
try {
children.value = await props.loadChildren()
childrenLoaded.value = true
} catch {
loadError.value = true
} finally {
loading.value = false
}
}
async function retry() {
await load()
}
async function toggleExpand() {
if (!expanded.value && !childrenLoaded.value) {
await load()
}
expanded.value = !expanded.value
}
/** Reload children if already expanded — called by parent when data changes. */
function refresh() {
if (expanded.value && childrenLoaded.value) {
load()
}
}
defineExpose({ refresh })
</script>