Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src-tauri/src/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ pub struct AppState {
pub indexing_task_handle: Mutex<Option<JoinHandle<()>>>,
pub lut_cache: Mutex<HashMap<String, Arc<Lut>>>,
pub initial_file_path: Mutex<Option<String>>,
pub initial_folder_path: Mutex<Option<String>>,
pub pending_edit_session: Mutex<Option<ExternalEditSession>>,
pub thumbnail_cancellation_token: Arc<AtomicBool>,
pub thumbnail_progress: Mutex<ThumbnailProgressTracker>,
Expand Down
24 changes: 20 additions & 4 deletions src-tauri/src/launch_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub struct HeadlessExportSession {
pub enum LaunchRequest {
None,
OpenFile(String),
OpenFolder(String),
EditSession(ExternalEditSession),
HeadlessExport(HeadlessExportSession),
InvalidHeadless(String),
Expand All @@ -37,6 +38,7 @@ pub enum LaunchRequest {
#[serde(rename_all = "camelCase")]
pub struct LaunchPayload {
pub open_with_file: Option<String>,
pub open_with_folder: Option<String>,
pub edit_session: Option<ExternalEditSession>,
}

Expand Down Expand Up @@ -156,17 +158,28 @@ pub fn parse_launch_args(args: &[String]) -> LaunchRequest {
}
(Some(source), None) => LaunchRequest::OpenFile(source),
_ => match plain {
Some(path) if PathBuf::from(&path).is_dir() => {
LaunchRequest::OpenFolder(canonical_folder_path(path))
}
Some(path) => LaunchRequest::OpenFile(path),
None => LaunchRequest::None,
},
}
}

fn handle_file_open(app_handle: &tauri::AppHandle, path: PathBuf) {
/// Путь папки без симлинков и лишних разделителей (иначе та же папка добавится в корни дважды).
pub fn canonical_folder_path(path: String) -> String {
std::fs::canonicalize(&path)
.ok()
.and_then(|p| p.to_str().map(str::to_string))
.unwrap_or(path)
}

fn emit_path_open(app_handle: &tauri::AppHandle, event: &str, path: PathBuf) {
if let Some(path_str) = path.to_str()
&& let Err(e) = app_handle.emit("open-with-file", path_str)
&& let Err(e) = app_handle.emit(event, path_str)
{
log::error!("Failed to emit open-with-file event: {}", e);
log::error!("Failed to emit {} event: {}", event, e);
}
}

Expand All @@ -178,7 +191,10 @@ pub fn emit_launch_request(app_handle: &tauri::AppHandle, request: LaunchRequest
}
}
LaunchRequest::OpenFile(path) => {
handle_file_open(app_handle, PathBuf::from(path));
emit_path_open(app_handle, "open-with-file", PathBuf::from(path));
}
LaunchRequest::OpenFolder(path) => {
emit_path_open(app_handle, "open-with-folder", PathBuf::from(path));
}
LaunchRequest::HeadlessExport(_) => {
cli_println!(
Expand Down
24 changes: 21 additions & 3 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1741,10 +1741,14 @@ fn frontend_ready(
}

let open_with_file = state.initial_file_path.lock().unwrap().take();
let open_with_folder = state.initial_folder_path.lock().unwrap().take();
let edit_session = state.pending_edit_session.lock().unwrap().take();
if let Some(path) = &open_with_file {
log::info!("Frontend is ready, returning initial path: {}", path);
}
if let Some(path) = &open_with_folder {
log::info!("Frontend is ready, returning initial folder: {}", path);
}
if let Some(session) = &edit_session {
log::info!(
"Frontend is ready, returning external edit session for: {}",
Expand All @@ -1753,6 +1757,7 @@ fn frontend_ready(
}
Ok(LaunchPayload {
open_with_file,
open_with_folder,
edit_session,
})
}
Expand Down Expand Up @@ -1853,6 +1858,10 @@ pub fn run() {
log::info!("Initial open: Storing path {} for later.", path);
*state.initial_file_path.lock().unwrap() = Some(path);
}
LaunchRequest::OpenFolder(path) => {
log::info!("Initial open: Storing folder {} for later.", path);
*state.initial_folder_path.lock().unwrap() = Some(path);
}
_ => {}
}
}
Expand Down Expand Up @@ -2163,6 +2172,7 @@ pub fn run() {
indexing_task_handle: Mutex::new(None),
lut_cache: Mutex::new(HashMap::new()),
initial_file_path: Mutex::new(None),
initial_folder_path: Mutex::new(None),
pending_edit_session: Mutex::new(None),
thumbnail_cancellation_token: Arc::new(AtomicBool::new(false)),
thumbnail_progress: Mutex::new(ThumbnailProgressTracker { total: 0, completed: 0 }),
Expand Down Expand Up @@ -2313,6 +2323,7 @@ pub fn run() {
tauri::RunEvent::Opened { urls } => {
if let Some(url) = urls.first()
&& let Ok(path) = url.to_file_path()
&& let path = std::fs::canonicalize(&path).unwrap_or(path)
&& let Some(path_str) = path.to_str()
{
let state = app_handle.state::<AppState>();
Expand All @@ -2331,11 +2342,18 @@ pub fn run() {

emit_launch_request(
app_handle,
LaunchRequest::OpenFile(path_str.to_string()),
if path.is_dir() {
LaunchRequest::OpenFolder(path_str.to_string())
} else {
LaunchRequest::OpenFile(path_str.to_string())
},
);
} else {
*state.initial_file_path.lock().unwrap() =
Some(path_str.to_string());
if path.is_dir() {
*state.initial_folder_path.lock().unwrap() = Some(path_str.to_string());
} else {
*state.initial_file_path.lock().unwrap() = Some(path_str.to_string());
}

log::info!(
"macOS initial open: Stored path {} for later.",
Expand Down
8 changes: 8 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ function App() {
handleSelectSubfolder,
handleSelectAlbum,
handleOpenFolder,
handleOpenFolderPath,
handleNavBack,
handleNavForward,
handleContinueSession,
Expand All @@ -361,6 +362,13 @@ function App() {
finishExternalEdit,
} = useExternalEditSession(handleImageSelect);

const initialFolderToOpen = useProcessStore((state) => state.initialFolderToOpen);
useEffect(() => {
if (!initialFolderToOpen) return;
useProcessStore.getState().setProcess({ initialFolderToOpen: null });
handleOpenFolderPath(initialFolderToOpen);
}, [initialFolderToOpen, handleOpenFolderPath]);

const {
handleRate,
handleClearSelection,
Expand Down
2 changes: 2 additions & 0 deletions src/hooks/useAppInitialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ export const useAppInitialization = ({
.then((launch: any) => {
if (launch?.editSession) {
useProcessStore.getState().setProcess({ externalEditSession: launch.editSession });
} else if (launch?.openWithFolder) {
useProcessStore.getState().setProcess({ initialFolderToOpen: launch.openWithFolder });
} else if (launch?.openWithFile) {
useProcessStore.getState().setProcess({ initialFileToOpen: launch.openWithFile });
}
Expand Down
75 changes: 54 additions & 21 deletions src/hooks/useAppNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,24 +508,41 @@ export function useAppNavigation({ clearThumbnailQueue, refs }: AppNavigationPro
}
}, [handleSelectSubfolder, handleSelectAlbum]);

const handleOpenFolder = useCallback(async () => {
const { osPlatform, appSettings, handleSettingsChange } = useSettingsStore.getState();
const { rootPaths, folderTrees, setLibrary } = useLibraryStore.getState();
const isAndroid = osPlatform === 'android';
const handleOpenFolderPath = useCallback(
async (selectedPath: string) => {
const { appSettings, handleSettingsChange } = useSettingsStore.getState();
const { setLibrary } = useLibraryStore.getState();
let { rootPaths, folderTrees } = useLibraryStore.getState();

try {
let selectedPath = '';
if (isAndroid) {
selectedPath = await invoke<string>(Invokes.GetOrCreateInternalLibraryRoot);
} else {
const selected = await open({ directory: true, multiple: false, defaultPath: await homeDir() });
if (typeof selected === 'string') {
selectedPath = selected;
try {
const showImageCounts =
appSettings?.enableFolderImageCounts || appSettings?.folderTreeSort?.key === 'imageCount';

// На холодном старте стор библиотеки ещё пуст: берём корни из настроек, а не затираем их.
if (rootPaths.length === 0 && appSettings?.rootFolders?.length) {
rootPaths = appSettings.rootFolders;
setLibrary({ rootPaths, expandedFolders: new Set(rootPaths), isTreeLoading: true });
try {
folderTrees = await invoke(Invokes.GetPinnedFolderTrees, {
paths: rootPaths,
expandedFolders: rootPaths,
showImageCounts,
});
setLibrary({ folderTrees });
} catch (err) {
console.error('Failed to load folder trees:', err);
} finally {
setLibrary({ isTreeLoading: false });
}
}
}

if (selectedPath) {
if (!rootPaths.includes(selectedPath)) {
const normalizePath = (p: string) => p.replace(/[\\/]+$/, '');
const targetPath = normalizePath(selectedPath);
const existingRoot = rootPaths.find((root) => {
const base = normalizePath(root);
return targetPath === base || targetPath.startsWith(`${base}/`) || targetPath.startsWith(`${base}\\`);
});
if (!existingRoot) {
const newRootPaths = [...rootPaths, selectedPath];
setLibrary({ rootPaths: newRootPaths });

Expand All @@ -538,23 +555,38 @@ export function useAppNavigation({ clearThumbnailQueue, refs }: AppNavigationPro
const newTree = await invoke(Invokes.GetFolderTree, {
path: selectedPath,
expandedFolders: [selectedPath],
showImageCounts:
appSettings?.enableFolderImageCounts || appSettings?.folderTreeSort?.key === 'imageCount',
showImageCounts,
});
setLibrary({ folderTrees: [...folderTrees, newTree] });
} catch (e) {
toast.error(`Failed to load folder tree: ${e}`);
} finally {
setLibrary({ isTreeLoading: false });
}
await handleSelectSubfolder(selectedPath, true);
} else {
await handleSelectSubfolder(selectedPath, false);
}
await handleSelectSubfolder(selectedPath, true);
} catch (err) {
console.error('Failed to open folder:', err);
toast.error('Failed to open the requested folder.');
}
},
[handleSelectSubfolder],
);

const handleOpenFolder = useCallback(async () => {
const { osPlatform } = useSettingsStore.getState();
const isAndroid = osPlatform === 'android';

try {
const selectedPath = isAndroid
? await invoke<string>(Invokes.GetOrCreateInternalLibraryRoot)
: await open({ directory: true, multiple: false, defaultPath: await homeDir() });
if (typeof selectedPath === 'string') await handleOpenFolderPath(selectedPath);
} catch (err) {
console.error(isAndroid ? 'Failed to open Android library root:' : 'Failed to open directory dialog:', err);
toast.error(isAndroid ? 'Failed to open library.' : 'Failed to open folder selection dialog.');
}
}, [handleSelectSubfolder]);
}, [handleOpenFolderPath]);

const handleContinueSession = () => {
const restore = async () => {
Expand Down Expand Up @@ -665,6 +697,7 @@ export function useAppNavigation({ clearThumbnailQueue, refs }: AppNavigationPro
handleSelectSubfolder,
handleSelectAlbum,
handleOpenFolder,
handleOpenFolderPath,
handleNavBack,
handleNavForward,
handleContinueSession,
Expand Down
3 changes: 3 additions & 0 deletions src/hooks/useTauriListeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ export function useTauriListeners({
listen('open-with-file', (event: any) => {
if (isEffectActive) useProcessStore.getState().setProcess({ initialFileToOpen: event.payload as string });
}),
listen('open-with-folder', (event: any) => {
if (isEffectActive) useProcessStore.getState().setProcess({ initialFolderToOpen: event.payload as string });
}),
listen('external-edit-session', (event: any) => {
if (isEffectActive) useProcessStore.getState().setProcess({ externalEditSession: event.payload });
}),
Expand Down
2 changes: 2 additions & 0 deletions src/store/useProcessStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ interface ProcessState {
isCopied: boolean;
isPasted: boolean;
initialFileToOpen: string | null;
initialFolderToOpen: string | null;
externalEditSession: ExternalEditSession | null;
activeAiTasks: Record<string, AiTask>;

Expand Down Expand Up @@ -62,6 +63,7 @@ export const useProcessStore = create<ProcessState>((set, get) => ({
isCopied: false,
isPasted: false,
initialFileToOpen: null,
initialFolderToOpen: null,
externalEditSession: null,
activeAiTasks: {},

Expand Down