feat: 添加路径收藏功能
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed, type PropType } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useFavoritePathsStore, type FavoritePathItem } from '../stores/favoritePaths.store';
|
||||
|
||||
const props = defineProps({
|
||||
isVisible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
pathData: {
|
||||
type: Object as PropType<FavoritePathItem | null>,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'saveSuccess']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const favoritePathsStore = useFavoritePathsStore();
|
||||
|
||||
const form = ref({
|
||||
id: '',
|
||||
path: '',
|
||||
name: '',
|
||||
});
|
||||
|
||||
const isEditMode = computed(() => !!props.pathData?.id);
|
||||
const isLoading = ref(false);
|
||||
const errorMessage = ref<string | null>(null);
|
||||
|
||||
|
||||
watch(() => props.isVisible, (newValue) => {
|
||||
if (newValue) {
|
||||
errorMessage.value = null; // Reset error on open
|
||||
if (props.pathData) {
|
||||
form.value = {
|
||||
id: props.pathData.id,
|
||||
path: props.pathData.path,
|
||||
name: props.pathData.name || ''
|
||||
};
|
||||
} else {
|
||||
form.value = { id: '', path: '', name: '' };
|
||||
}
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
if (!form.value.path.trim()) {
|
||||
errorMessage.value = t('favoritePaths.addEditForm.validation.pathRequired', 'Path is required.');
|
||||
return false;
|
||||
}
|
||||
// Add other validation rules if needed
|
||||
errorMessage.value = null;
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
isLoading.value = true;
|
||||
errorMessage.value = null;
|
||||
try {
|
||||
if (isEditMode.value && form.value.id) {
|
||||
await favoritePathsStore.updateFavoritePath(form.value.id, {
|
||||
path: form.value.path,
|
||||
name: form.value.name || undefined, // Send undefined if empty to allow backend to handle
|
||||
}, t);
|
||||
} else {
|
||||
await favoritePathsStore.addFavoritePath({
|
||||
path: form.value.path,
|
||||
name: form.value.name || undefined,
|
||||
}, t);
|
||||
}
|
||||
emit('saveSuccess');
|
||||
closeModal();
|
||||
} catch (error: any) {
|
||||
console.error('Error saving favorite path:', error);
|
||||
errorMessage.value = error.message || t('favoritePaths.addEditForm.errors.genericSaveError', 'Failed to save favorite path.');
|
||||
// Notification is usually handled by the store, but we can show a local error too.
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
if (!isLoading.value) { // Prevent closing while loading
|
||||
emit('close');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="isVisible"
|
||||
class="fixed inset-0 z-[60] flex items-center justify-center bg-[var(--overlay-bg-color)]"
|
||||
@click.self="closeModal"
|
||||
>
|
||||
<div class="bg-background text-foreground shadow-xl rounded-lg w-full max-w-md flex flex-col overflow-hidden m-4 p-6">
|
||||
<!-- Header -->
|
||||
<h2 class="m-0 mb-6 text-center text-xl font-semibold">
|
||||
{{ isEditMode ? t('favoritePaths.addEditForm.editTitle', 'Edit Favorite Path') : t('favoritePaths.addEditForm.addTitle', 'Add New Favorite Path') }}
|
||||
</h2>
|
||||
|
||||
<!-- Form Body -->
|
||||
<form @submit.prevent="handleSubmit" class="space-y-4 flex-grow overflow-y-auto">
|
||||
|
||||
<div>
|
||||
<label for="favPath-name" class="block text-sm font-medium text-text-secondary mb-1">
|
||||
{{ t('favoritePaths.addEditForm.nameLabel', 'Name (Optional)') }}
|
||||
</label>
|
||||
<input
|
||||
id="favPath-name"
|
||||
type="text"
|
||||
v-model="form.name"
|
||||
:disabled="isLoading"
|
||||
class="w-full bg-input border border-border rounded-md px-3 py-2 text-sm outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-colors"
|
||||
:placeholder="t('favoritePaths.addEditForm.namePlaceholder', 'My Documents')"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="favPath-path" class="block text-sm font-medium text-text-secondary mb-1">
|
||||
{{ t('favoritePaths.addEditForm.pathLabel', 'Path') }}
|
||||
<span class="text-danger ml-0.5">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="favPath-path"
|
||||
type="text"
|
||||
v-model="form.path"
|
||||
required
|
||||
:disabled="isLoading"
|
||||
class="w-full bg-input border border-border rounded-md px-3 py-2 text-sm outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-colors"
|
||||
:placeholder="t('favoritePaths.addEditForm.pathPlaceholder', '/example/folder/path')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="errorMessage" class="text-danger text-sm p-2 bg-danger/10 rounded-md">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex justify-end mt-8 pt-4 border-t border-border/50">
|
||||
<!-- Secondary/Cancel Button -->
|
||||
<button
|
||||
type="button"
|
||||
@click="closeModal"
|
||||
:disabled="isLoading"
|
||||
class="py-2 px-5 rounded-lg text-sm font-medium transition-colors duration-150 bg-background border border-border/50 text-text-secondary hover:bg-border hover:text-foreground mr-3">
|
||||
{{ t('common.cancel', 'Cancel') }}
|
||||
</button>
|
||||
<!-- Primary/Submit Button -->
|
||||
<button
|
||||
type="submit"
|
||||
@click="handleSubmit"
|
||||
:disabled="isLoading || !form.path.trim()"
|
||||
class="py-2 px-5 rounded-lg text-sm font-semibold transition-colors duration-150 bg-primary text-white border-none shadow-md hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:bg-gray-400 disabled:opacity-70 disabled:cursor-not-allowed">
|
||||
{{ isLoading ? t('common.saving', 'Saving...') : t('common.save', 'Save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Styles are primarily Tailwind based */
|
||||
.bg-background-hover:hover {
|
||||
background-color: var(--color-bg-hover); /* Ensure this CSS variable is defined globally or in Tailwind config */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,283 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch, onBeforeUnmount, nextTick, type PropType } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useFavoritePathsStore, type FavoritePathItem } from '../stores/favoritePaths.store';
|
||||
import { useSessionStore } from '../stores/session.store';
|
||||
import AddEditFavoritePathForm from './AddEditFavoritePathForm.vue';
|
||||
|
||||
const PADDING = 8; // px
|
||||
|
||||
const props = defineProps({
|
||||
isVisible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
triggerElement: {
|
||||
type: Object as PropType<HTMLElement | null>,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'navigateToPath']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const favoritePathsStore = useFavoritePathsStore();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
const searchTerm = ref('');
|
||||
const showAddEditModal = ref(false);
|
||||
const editingPathItem = ref<FavoritePathItem | null>(null);
|
||||
const modalContentRef = ref<HTMLElement | null>(null);
|
||||
const modalStyle = ref<Record<string, string>>({});
|
||||
|
||||
|
||||
const filteredPaths = computed(() => {
|
||||
if (!searchTerm.value) {
|
||||
return favoritePathsStore.favoritePaths;
|
||||
}
|
||||
const lowerSearchTerm = searchTerm.value.toLowerCase();
|
||||
return favoritePathsStore.favoritePaths.filter(
|
||||
(p) =>
|
||||
p.path.toLowerCase().includes(lowerSearchTerm) ||
|
||||
(p.name && p.name.toLowerCase().includes(lowerSearchTerm))
|
||||
);
|
||||
});
|
||||
|
||||
// Computed property for sort button icon and title
|
||||
const currentSortBy = computed(() => favoritePathsStore.currentSortBy);
|
||||
|
||||
const sortButtonIcon = computed(() => {
|
||||
return currentSortBy.value === 'name' ? 'fas fa-sort-alpha-down' : 'fas fa-clock';
|
||||
});
|
||||
|
||||
|
||||
|
||||
const toggleSort = () => {
|
||||
const newSortBy = currentSortBy.value === 'name' ? 'last_used_at' : 'name';
|
||||
favoritePathsStore.setSortBy(newSortBy);
|
||||
};
|
||||
|
||||
const handleItemClick = async (pathItem: FavoritePathItem) => {
|
||||
try {
|
||||
// Mark path as used before navigating
|
||||
await favoritePathsStore.markPathAsUsed(pathItem.id, t);
|
||||
} catch (error) {
|
||||
console.error('Failed to mark path as used:', error);
|
||||
// Optionally, inform the user about the failure, though navigation will still proceed.
|
||||
}
|
||||
emit('navigateToPath', pathItem.path);
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const openAddModal = () => {
|
||||
editingPathItem.value = null;
|
||||
showAddEditModal.value = true;
|
||||
};
|
||||
|
||||
const openEditModal = (pathItem: FavoritePathItem) => {
|
||||
editingPathItem.value = { ...pathItem };
|
||||
showAddEditModal.value = true;
|
||||
};
|
||||
|
||||
const handleDelete = async (pathItem: FavoritePathItem) => {
|
||||
if (confirm(t('favoritePaths.confirmDelete', { name: pathItem.name || pathItem.path }))) {
|
||||
try {
|
||||
await favoritePathsStore.deleteFavoritePath(pathItem.id, t);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete favorite path from modal:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const updatePosition = () => {
|
||||
if (!props.isVisible || !props.triggerElement || !modalContentRef.value) {
|
||||
// If not visible or refs not available, do nothing or hide.
|
||||
// v-if handles DOM presence, so style isn't applied when not isVisible.
|
||||
return;
|
||||
}
|
||||
|
||||
const triggerRect = props.triggerElement.getBoundingClientRect();
|
||||
const modalWidth = modalContentRef.value.offsetWidth;
|
||||
const modalHeight = modalContentRef.value.offsetHeight;
|
||||
|
||||
// If dimensions are zero when modal is supposed to be visible,
|
||||
// it might mean content affecting size isn't ready. Retry once.
|
||||
if (modalWidth === 0 && modalHeight === 0 && props.isVisible) {
|
||||
nextTick(updatePosition);
|
||||
return;
|
||||
}
|
||||
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
let top = triggerRect.bottom + 2; // Default position below trigger, with a small 2px gap
|
||||
let left = triggerRect.left;
|
||||
|
||||
// Check for bottom overflow
|
||||
if (top + modalHeight + PADDING > viewportHeight) {
|
||||
// Try to position above the trigger
|
||||
top = triggerRect.top - modalHeight - 2; // Position above trigger, with a small 2px gap
|
||||
}
|
||||
|
||||
// If positioning above also causes top overflow (e.g., trigger is near the top and modal is tall)
|
||||
if (top < PADDING) {
|
||||
top = PADDING; // Align to viewport top with padding
|
||||
// Note: If modalHeight is still greater than viewportHeight - 2*PADDING,
|
||||
// it will overflow downwards. The `max-h-80` class on the modal
|
||||
// should generally prevent the modal itself from being excessively tall.
|
||||
}
|
||||
|
||||
// Check for right overflow
|
||||
if (left + modalWidth + PADDING > viewportWidth) {
|
||||
left = viewportWidth - modalWidth - PADDING; // Align to viewport right edge
|
||||
}
|
||||
|
||||
// Check for left overflow (less likely with initial left alignment to trigger, but good for robustness)
|
||||
if (left < PADDING) {
|
||||
left = PADDING; // Align to viewport left edge
|
||||
}
|
||||
|
||||
modalStyle.value = {
|
||||
position: 'fixed', // Position relative to the viewport
|
||||
top: `${top}px`,
|
||||
left: `${left}px`,
|
||||
};
|
||||
};
|
||||
|
||||
// --- Click Outside Logic ---
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (modalContentRef.value && !modalContentRef.value.contains(event.target as Node)) {
|
||||
if (!showAddEditModal.value) { // Do not close if add/edit modal is open
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => props.isVisible, (newValue: boolean) => {
|
||||
if (newValue) {
|
||||
favoritePathsStore.fetchFavoritePaths(t);
|
||||
searchTerm.value = '';
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
nextTick(() => { // Ensure DOM is ready for measurements
|
||||
updatePosition(); // Calculate initial position
|
||||
window.addEventListener('resize', updatePosition); // Adjust position on window resize
|
||||
});
|
||||
} else {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
window.removeEventListener('resize', updatePosition); // Clean up resize listener
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (props.isVisible) {
|
||||
favoritePathsStore.fetchFavoritePaths(t);
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
window.removeEventListener('resize', updatePosition); // Ensure resize listener is cleaned up
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- New single root element -->
|
||||
<div>
|
||||
<!-- Favorite Paths Dropdown -->
|
||||
<div
|
||||
v-if="isVisible"
|
||||
ref="modalContentRef"
|
||||
:style="modalStyle"
|
||||
class="z-50 w-72 md:w-80 rounded-md bg-background shadow-lg border border-border/50 max-h-80 flex flex-col overflow-hidden"
|
||||
>
|
||||
<!-- Toolbar: Search and Add Button -->
|
||||
<div class="p-2 flex-shrink-0 flex items-center gap-2">
|
||||
<div class="relative flex-grow">
|
||||
<input
|
||||
type="text"
|
||||
v-model="searchTerm"
|
||||
:placeholder="t('favoritePaths.searchPlaceholder', 'Search by name or path...')"
|
||||
class="w-full bg-input border border-border rounded-md pl-2.5 pr-2 py-1.5 text-sm outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
@click="toggleSort"
|
||||
class="flex items-center justify-center w-8 h-8 bg-background border border-border text-text-secondary rounded-lg text-sm cursor-pointer shadow-sm transition-colors duration-200 ease-in-out hover:bg-primary/10 hover:text-primary focus:outline-none flex-shrink-0"
|
||||
>
|
||||
<i :class="sortButtonIcon"></i>
|
||||
</button>
|
||||
<button
|
||||
@click="openAddModal"
|
||||
class="flex items-center justify-center w-8 h-8 bg-primary text-white border-none rounded-lg text-sm font-semibold cursor-pointer shadow-md transition-colors duration-200 ease-in-out hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary flex-shrink-0"
|
||||
:title="t('favoritePaths.addNew', 'Add new favorite path')"
|
||||
>
|
||||
<i class="fas fa-plus text-base"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Path List -->
|
||||
<div class="overflow-y-auto flex-grow p-1 text-sm">
|
||||
<ul v-if="!favoritePathsStore.isLoading && favoritePathsStore.filteredFavoritePaths.length" class="list-none m-0 p-0">
|
||||
<li
|
||||
v-for="favPath in favoritePathsStore.filteredFavoritePaths"
|
||||
:key="favPath.id"
|
||||
class="p-2 hover:bg-primary/10 cursor-pointer group flex items-center justify-between rounded-md transition-colors duration-150"
|
||||
@click="handleItemClick(favPath)"
|
||||
:title="favPath.path"
|
||||
>
|
||||
<div class="flex-grow overflow-hidden mr-2">
|
||||
<p class="font-medium truncate text-foreground">
|
||||
{{ favPath.name || favPath.path }}
|
||||
</p>
|
||||
<p v-if="favPath.name" class="text-xs text-text-secondary truncate">
|
||||
{{ favPath.path }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-shrink-0 flex items-center gap-1 opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity duration-150">
|
||||
<button
|
||||
@click.stop="openEditModal(favPath)"
|
||||
class="p-1.5 rounded text-text-secondary hover:text-primary hover:bg-black/10 dark:hover:bg-white/10 transition-colors"
|
||||
:title="t('common.edit')">
|
||||
<i class="fas fa-pencil-alt text-xs"></i>
|
||||
</button>
|
||||
<button
|
||||
@click.stop="handleDelete(favPath)"
|
||||
class="p-1.5 rounded text-text-secondary hover:text-error hover:bg-black/10 dark:hover:bg-white/10 transition-colors"
|
||||
:title="t('common.delete')">
|
||||
<i class="fas fa-trash-alt text-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else-if="favoritePathsStore.isLoading" class="p-3 text-center text-text-secondary">
|
||||
<i class="fas fa-spinner fa-spin mr-1"></i>
|
||||
{{ t('favoritePaths.loading', 'Loading favorites...') }}
|
||||
</div>
|
||||
<div v-else class="p-3 text-center text-text-secondary">
|
||||
<i class="fas fa-star-half-alt mr-1"></i>
|
||||
{{ searchTerm ? t('favoritePaths.noResults', 'No matching favorites found.') : t('favoritePaths.noFavorites', 'No favorite paths yet. Add one!') }}
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- End of Favorite Paths Dropdown div -->
|
||||
|
||||
<!-- Add/Edit Modal -->
|
||||
<AddEditFavoritePathForm
|
||||
v-if="showAddEditModal"
|
||||
:is-visible="showAddEditModal"
|
||||
:path-data="editingPathItem"
|
||||
@close="showAddEditModal = false"
|
||||
@save-success="() => { favoritePathsStore.fetchFavoritePaths(t); showAddEditModal = false; }"
|
||||
/>
|
||||
</div> <!-- End of new single root element -->
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Styles are mostly Tailwind. max-h-80 is applied to the main div */
|
||||
/* Styling for the add/edit form if it's part of this component's template directly and needs specific scoping */
|
||||
</style>
|
||||
@@ -18,8 +18,9 @@ import FileManagerContextMenu from './FileManagerContextMenu.vue';
|
||||
import FileManagerActionModal from './FileManagerActionModal.vue';
|
||||
import type { FileListItem } from '../types/sftp.types';
|
||||
import type { WebSocketMessage } from '../types/websocket.types';
|
||||
import PathHistoryDropdown from './PathHistoryDropdown.vue';
|
||||
import { usePathHistoryStore } from '../stores/pathHistory.store';
|
||||
import PathHistoryDropdown from './PathHistoryDropdown.vue';
|
||||
import { usePathHistoryStore } from '../stores/pathHistory.store';
|
||||
import FavoritePathsModal from './FavoritePathsModal.vue'; // +++ Import FavoritePathsModal +++
|
||||
|
||||
|
||||
type SftpManagerInstance = ReturnType<typeof createSftpActionsManager>;
|
||||
@@ -130,6 +131,10 @@ const fileListContainerRef = ref<HTMLDivElement | null>(null); // 文件列表
|
||||
const dropOverlayRef = ref<HTMLDivElement | null>(null); // +++ 拖拽蒙版引用 +++
|
||||
// const scrollIntervalId = ref<number | null>(null); // 已移至 useFileManagerDragAndDrop
|
||||
|
||||
// +++ Favorite Paths Modal State +++
|
||||
const showFavoritePathsModal = ref(false);
|
||||
const favoritePathsButtonRef = ref<HTMLButtonElement | null>(null); // Ref for the trigger button
|
||||
|
||||
// +++ Path History Refs +++
|
||||
const showPathHistoryDropdown = ref(false);
|
||||
const pathInputWrapperRef = ref<HTMLDivElement | null>(null); // Wrapper for path input and dropdown
|
||||
@@ -1548,8 +1553,23 @@ const handleOpenEditorClick = () => {
|
||||
// 暂时使用 triggerPopup,传递空字符串表示空编辑器
|
||||
// 后续可能需要 fileEditorStore.triggerEmptyPopup(props.sessionId);
|
||||
fileEditorStore.triggerPopup('', props.sessionId); // 修复:传递空字符串而不是 null
|
||||
};
|
||||
</script>
|
||||
};
|
||||
|
||||
// +++ Favorite Paths Modal Logic +++
|
||||
const toggleFavoritePathsModal = () => {
|
||||
showFavoritePathsModal.value = !showFavoritePathsModal.value;
|
||||
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Toggled FavoritePathsModal. Visible: ${showFavoritePathsModal.value}`);
|
||||
};
|
||||
|
||||
const handleNavigateToPathFromFavorites = (path: string) => {
|
||||
if (currentSftpManager.value) {
|
||||
currentSftpManager.value.loadDirectory(path);
|
||||
// Optionally, add to local path history if not already handled by the store/modal
|
||||
// pathHistoryStore.addPath(path);
|
||||
}
|
||||
showFavoritePathsModal.value = false; // Close modal after navigation
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full overflow-hidden bg-background text-foreground text-sm font-sans">
|
||||
@@ -1615,13 +1635,31 @@ const handleOpenEditorClick = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- End Path Actions -->
|
||||
<!-- Wrapper for Favorite Paths Button and Modal -->
|
||||
<div class="relative flex-shrink-0">
|
||||
<!-- Favorite Paths Button -->
|
||||
<button
|
||||
ref="favoritePathsButtonRef"
|
||||
class="flex items-center justify-center w-7 h-7 text-text-secondary rounded transition-colors duration-200 hover:enabled:bg-black/10 hover:enabled:text-foreground"
|
||||
@click="toggleFavoritePathsModal"
|
||||
>
|
||||
<i class="fas fa-star text-base"></i>
|
||||
</button>
|
||||
<!-- Favorite Paths Modal -->
|
||||
<FavoritePathsModal
|
||||
:is-visible="showFavoritePathsModal"
|
||||
:trigger-element="favoritePathsButtonRef"
|
||||
@close="showFavoritePathsModal = false"
|
||||
@navigate-to-path="handleNavigateToPathFromFavorites"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Path Bar with History Dropdown -->
|
||||
<div ref="pathInputWrapperRef" class="relative flex items-center bg-background border border-border rounded px-1.5 py-0.5 min-w-[100px] flex-shrink">
|
||||
<span v-show="!isEditingPath && !showPathHistoryDropdown" @click="startPathEdit" class="text-text-secondary whitespace-nowrap overflow-x-auto pr-2 cursor-text">
|
||||
<span v-if="!props.isMobile">{{ t('fileManager.currentPath') }}:</span>
|
||||
<strong
|
||||
:title="t('fileManager.editPathTooltip')"
|
||||
class="font-medium text-link ml-1 px-1 rounded transition-colors duration-200"
|
||||
class="font-medium text-link px-1 rounded transition-colors duration-200"
|
||||
:class="{
|
||||
'hover:bg-black/5': currentSftpManager && props.wsDeps.isConnected.value,
|
||||
'opacity-60 cursor-not-allowed': !currentSftpManager || !props.wsDeps.isConnected.value
|
||||
@@ -1902,8 +1940,10 @@ const handleOpenEditorClick = () => {
|
||||
@confirm="handleModalConfirm"
|
||||
/>
|
||||
|
||||
<!-- Favorite Paths Modal is now positioned near its button -->
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user