first commit
This commit is contained in:
541
src/redux/fileManagerSlice.ts
Executable file
541
src/redux/fileManagerSlice.ts
Executable file
@@ -0,0 +1,541 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { FileResponse, FileThumbResponse, FileType, ListResponse } from "../api/explorer.ts";
|
||||
import { AppError, Response } from "../api/request.ts";
|
||||
import { Capacity } from "../api/user.ts";
|
||||
import { ListViewColumnSetting } from "../component/FileManager/Explorer/ListView/Column.tsx";
|
||||
import SessionManager, { UserSettings } from "../session";
|
||||
import { SearchParam } from "../util/uri.ts";
|
||||
|
||||
export const ContextMenuTypes = {
|
||||
empty: "empty",
|
||||
new: "new",
|
||||
file: "file",
|
||||
searchResult: "searchResult",
|
||||
};
|
||||
|
||||
export interface SingleManager {
|
||||
path?: string;
|
||||
previous_path?: string;
|
||||
pure_path?: string;
|
||||
pure_path_with_category?: string;
|
||||
path_root?: string;
|
||||
path_root_with_category?: string;
|
||||
path_elements?: string[];
|
||||
search_params?: SearchParam;
|
||||
showError: boolean;
|
||||
error?: Response<any>;
|
||||
loading: boolean;
|
||||
list?: ListResponse;
|
||||
tree: {
|
||||
[key: string]: FmTreeItem;
|
||||
};
|
||||
current_fs?: string;
|
||||
capacity?: Capacity;
|
||||
selected: {
|
||||
[key: string]: FileResponse;
|
||||
};
|
||||
|
||||
// View
|
||||
layout?: string;
|
||||
showThumb: boolean;
|
||||
pageSize: number;
|
||||
sortBy?: string;
|
||||
sortDirection?: string;
|
||||
multiSelectHovered: {
|
||||
[key: string]: boolean;
|
||||
};
|
||||
galleryWidth: number;
|
||||
|
||||
// Context Menu
|
||||
contextMenuOpen?: boolean;
|
||||
contextMenuPos?: { x: number; y: number };
|
||||
contextMenuType?: string;
|
||||
contextMenuTargets?: {
|
||||
[key: string]: FileResponse;
|
||||
};
|
||||
contextMenuTargetFm?: number;
|
||||
|
||||
// Dialogs
|
||||
|
||||
// Delete file dialog
|
||||
deleteFileModalOpen?: boolean;
|
||||
deleteFileModalSelected?: FileResponse[];
|
||||
deleteFileModalPromiseId?: string;
|
||||
deleteFileModalLoading?: boolean;
|
||||
|
||||
// Rename file dialog
|
||||
renameFileModalOpen?: boolean;
|
||||
renameFileModalSelected?: FileResponse;
|
||||
renameFileModalPromiseId?: string;
|
||||
renameFileModalLoading?: boolean;
|
||||
renameFileModalError?: string;
|
||||
|
||||
// List view
|
||||
listViewColumns: ListViewColumnSetting[];
|
||||
}
|
||||
|
||||
export const Layouts = {
|
||||
grid: "grid",
|
||||
list: "list",
|
||||
gallery: "gallery",
|
||||
};
|
||||
|
||||
export interface FmTreeItem {
|
||||
file?: FileResponse;
|
||||
thumb?: ThumbCache;
|
||||
children?: string[];
|
||||
}
|
||||
|
||||
export interface ThumbCache {
|
||||
src: string;
|
||||
expires?: string;
|
||||
}
|
||||
|
||||
export interface FileManagerArgsBase<T> {
|
||||
index: number;
|
||||
value: T;
|
||||
}
|
||||
|
||||
const defaultManagerValue = {
|
||||
loading: false,
|
||||
showError: false,
|
||||
tree: {},
|
||||
root_meta: {},
|
||||
selected: {},
|
||||
pageSize: SessionManager.getWithFallback(UserSettings.PageSize),
|
||||
layout: SessionManager.getWithFallback(UserSettings.Layout),
|
||||
showThumb: SessionManager.getWithFallback(UserSettings.ShowThumb),
|
||||
sortBy: SessionManager.getWithFallback(UserSettings.SortBy),
|
||||
sortDirection: SessionManager.getWithFallback(UserSettings.SortDirection),
|
||||
listViewColumns: SessionManager.getWithFallback(UserSettings.ListViewColumns),
|
||||
multiSelectHovered: {},
|
||||
galleryWidth: window.matchMedia("(max-width: 600px)")?.matches
|
||||
? 110
|
||||
: SessionManager.getWithFallback(UserSettings.GalleryWidth),
|
||||
};
|
||||
const initialState: [SingleManager, SingleManager] = [defaultManagerValue, defaultManagerValue];
|
||||
|
||||
export const fileManagerSlice = createSlice({
|
||||
name: "fileManagerSlice",
|
||||
initialState,
|
||||
reducers: {
|
||||
setGalleryWidth: (state, action: PayloadAction<FileManagerArgsBase<number>>) => {
|
||||
state[action.payload.index].galleryWidth = action.payload.value;
|
||||
},
|
||||
setListViewColumns: (state, action: PayloadAction<ListViewColumnSetting[]>) => {
|
||||
state.forEach((_fm, index) => {
|
||||
state[index].listViewColumns = action.payload;
|
||||
});
|
||||
},
|
||||
removeThumbCache: (state, action: PayloadAction<FileManagerArgsBase<string[]>>) => {
|
||||
action.payload.value.forEach((path) => {
|
||||
state[action.payload.index].tree[path].thumb = undefined;
|
||||
});
|
||||
},
|
||||
resetFileManager: (state, action: PayloadAction<number>) => {
|
||||
state[action.payload].path = undefined;
|
||||
state[action.payload].previous_path = undefined;
|
||||
state[action.payload].pure_path = undefined;
|
||||
state[action.payload].pure_path_with_category = undefined;
|
||||
state[action.payload].search_params = undefined;
|
||||
state[action.payload].path_root = undefined;
|
||||
state[action.payload].path_root_with_category = undefined;
|
||||
state[action.payload].path_elements = undefined;
|
||||
state[action.payload].showError = false;
|
||||
state[action.payload].error = undefined;
|
||||
state[action.payload].list = undefined;
|
||||
state[action.payload].selected = {};
|
||||
state[action.payload].contextMenuOpen = false;
|
||||
state[action.payload].deleteFileModalOpen = state[action.payload].deleteFileModalOpen ? false : undefined;
|
||||
state[action.payload].renameFileModalOpen = state[action.payload].renameFileModalOpen ? false : undefined;
|
||||
},
|
||||
setFmError: (state, action: PayloadAction<FileManagerArgsBase<any>>) => {
|
||||
const e = action.payload.value;
|
||||
state[action.payload.index].showError = !!e;
|
||||
if (e instanceof AppError) {
|
||||
state[action.payload.index].error = e.ErrorResponse();
|
||||
} else if (e instanceof Error) {
|
||||
state[action.payload.index].error = {
|
||||
msg: e.message,
|
||||
code: -1,
|
||||
data: undefined,
|
||||
};
|
||||
}
|
||||
},
|
||||
setFmLoading: (state, action: PayloadAction<FileManagerArgsBase<boolean>>) => {
|
||||
state[action.payload.index].loading = action.payload.value;
|
||||
},
|
||||
applyListResponse: (state, action: PayloadAction<FileManagerArgsBase<ListResponse>>) => {
|
||||
state[action.payload.index].list = action.payload.value;
|
||||
},
|
||||
appendListResponse: (state, action: PayloadAction<FileManagerArgsBase<ListResponse>>) => {
|
||||
const newList = [...(state[action.payload.index].list?.files || []), ...action.payload.value.files];
|
||||
state[action.payload.index].list = {
|
||||
...action.payload.value,
|
||||
files: newList,
|
||||
};
|
||||
},
|
||||
setFileList: (state, action: PayloadAction<FileManagerArgsBase<FileResponse[]>>) => {
|
||||
const fm = state[action.payload.index];
|
||||
if (fm.list) {
|
||||
fm.list.files = action.payload.value;
|
||||
}
|
||||
},
|
||||
appendTreeCache: (state, action: PayloadAction<FileManagerArgsBase<[FileResponse[], string | undefined]>>) => {
|
||||
const path = action.payload.value[1];
|
||||
const fm = state[action.payload.index];
|
||||
action.payload.value[0].forEach((file) => {
|
||||
if (!fm.tree[file.path]) {
|
||||
fm.tree[file.path] = {};
|
||||
}
|
||||
fm.tree[file.path].file = file;
|
||||
});
|
||||
|
||||
if (!path) return;
|
||||
|
||||
if (!fm.tree[path]) {
|
||||
fm.tree[path] = {};
|
||||
}
|
||||
fm.tree[path].children = [
|
||||
...new Set([
|
||||
...(fm.tree[path].children ?? []),
|
||||
...action.payload.value[0].filter((t) => t.type == FileType.folder).map((file) => file.path),
|
||||
]),
|
||||
];
|
||||
},
|
||||
removeTreeCache: (state, action: PayloadAction<FileManagerArgsBase<string[]>>) => {
|
||||
const path = action.payload.value;
|
||||
const fm = state[action.payload.index];
|
||||
// Recursively delete children
|
||||
const deleteChildren = (p: string) => {
|
||||
if (fm.tree[p]?.children) {
|
||||
fm.tree[p].children?.forEach((c) => {
|
||||
deleteChildren(c);
|
||||
delete state[action.payload.index].tree[c];
|
||||
});
|
||||
}
|
||||
};
|
||||
path.forEach((p) => {
|
||||
deleteChildren(p);
|
||||
delete state[action.payload.index].tree[p];
|
||||
// Delete path from parent's child
|
||||
const parentPath = p.substring(0, p.lastIndexOf("/"));
|
||||
if (parentPath && state[action.payload.index].tree[parentPath]) {
|
||||
state[action.payload.index].tree[parentPath].children = state[action.payload.index].tree[
|
||||
parentPath
|
||||
].children?.filter((pa) => pa != p);
|
||||
}
|
||||
});
|
||||
},
|
||||
setPathProps: (
|
||||
state,
|
||||
action: PayloadAction<
|
||||
FileManagerArgsBase<{
|
||||
path: string;
|
||||
path_elements: string[];
|
||||
path_root: string;
|
||||
current_fs: string;
|
||||
pure_path: string;
|
||||
pure_path_with_category: string;
|
||||
path_root_with_category: string;
|
||||
search_params?: SearchParam;
|
||||
}>
|
||||
>,
|
||||
) => {
|
||||
if (state[action.payload.index].path != action.payload.value.path) {
|
||||
state[action.payload.index].previous_path = state[action.payload.index].path;
|
||||
}
|
||||
state[action.payload.index].path = action.payload.value.path;
|
||||
state[action.payload.index].path_elements = action.payload.value.path_elements;
|
||||
state[action.payload.index].path_root = action.payload.value.path_root;
|
||||
state[action.payload.index].current_fs = action.payload.value.current_fs;
|
||||
state[action.payload.index].pure_path_with_category = action.payload.value.pure_path_with_category;
|
||||
state[action.payload.index].pure_path = action.payload.value.pure_path;
|
||||
state[action.payload.index].path_root_with_category = action.payload.value.path_root_with_category;
|
||||
state[action.payload.index].search_params = action.payload.value.search_params;
|
||||
},
|
||||
setLayout: (state, action: PayloadAction<FileManagerArgsBase<string>>) => {
|
||||
state[action.payload.index].layout = action.payload.value;
|
||||
},
|
||||
setShowThumb: (state, action: PayloadAction<FileManagerArgsBase<boolean>>) => {
|
||||
state[action.payload.index].showThumb = action.payload.value;
|
||||
},
|
||||
setPageSize: (state, action: PayloadAction<FileManagerArgsBase<number>>) => {
|
||||
const index = action.payload.index;
|
||||
state[index].pageSize = action.payload.value;
|
||||
if (state[index].list?.pagination.next_token) {
|
||||
// @ts-ignore
|
||||
state[index].list.pagination.next_token = undefined;
|
||||
}
|
||||
},
|
||||
setSortOption: (state, action: PayloadAction<FileManagerArgsBase<[string, string]>>) => {
|
||||
const index = action.payload.index;
|
||||
state[index].sortBy = action.payload.value[0];
|
||||
state[index].sortDirection = action.payload.value[1];
|
||||
if (state[index].list?.pagination.next_token) {
|
||||
// @ts-ignore
|
||||
state[index].list.pagination.next_token = undefined;
|
||||
}
|
||||
},
|
||||
setThumbCache: (state, action: PayloadAction<FileManagerArgsBase<[string, FileThumbResponse]>>) => {
|
||||
const index = action.payload.index;
|
||||
const path = action.payload.value[0];
|
||||
const thumb = action.payload.value[1];
|
||||
if (!state[index].tree[path]) {
|
||||
state[index].tree[path] = {};
|
||||
}
|
||||
state[index].tree[path].thumb = {
|
||||
src: thumb?.url,
|
||||
expires: thumb?.expires,
|
||||
};
|
||||
},
|
||||
clearSessionCache: (state, action: PayloadAction<FileManagerArgsBase<any>>) => {
|
||||
const index = action.payload.index;
|
||||
state[index].tree = {};
|
||||
state[index].capacity = undefined;
|
||||
},
|
||||
setCapacity: (state, action: PayloadAction<FileManagerArgsBase<Capacity>>) => {
|
||||
const index = action.payload.index;
|
||||
state[index].capacity = action.payload.value;
|
||||
},
|
||||
setPage: (state, action: PayloadAction<FileManagerArgsBase<number>>) => {
|
||||
const s = state[action.payload.index];
|
||||
if (s.list?.pagination) {
|
||||
s.list.pagination.page = action.payload.value;
|
||||
}
|
||||
},
|
||||
addSelected: (state, action: PayloadAction<FileManagerArgsBase<FileResponse[]>>) => {
|
||||
const index = action.payload.index;
|
||||
action.payload.value.forEach((file) => {
|
||||
state[index].selected[file.path] = file;
|
||||
});
|
||||
},
|
||||
removeSelected: (state, action: PayloadAction<FileManagerArgsBase<string>>) => {
|
||||
const index = action.payload.index;
|
||||
delete state[index].selected[action.payload.value];
|
||||
},
|
||||
clearSelected: (state, action: PayloadAction<FileManagerArgsBase<any>>) => {
|
||||
const index = action.payload.index;
|
||||
state[index].selected = {};
|
||||
},
|
||||
setSelected: (state, action: PayloadAction<FileManagerArgsBase<FileResponse[]>>) => {
|
||||
const index = action.payload.index;
|
||||
state[index].selected = {};
|
||||
action.payload.value.forEach((file) => {
|
||||
state[index].selected[file.path] = file;
|
||||
});
|
||||
},
|
||||
setMultiSelectHovered: (state, action: PayloadAction<FileManagerArgsBase<[string, boolean]>>) => {
|
||||
const index = action.payload.index;
|
||||
const [path, hovered] = action.payload.value;
|
||||
if (!hovered) {
|
||||
delete state[index].multiSelectHovered[path];
|
||||
} else {
|
||||
state[index].multiSelectHovered = {};
|
||||
state[index].multiSelectHovered[path] = hovered;
|
||||
}
|
||||
},
|
||||
clearMultiSelectHovered: (state, action: PayloadAction<FileManagerArgsBase<any>>) => {
|
||||
const index = action.payload.index;
|
||||
state[index].multiSelectHovered = {};
|
||||
},
|
||||
setContextMenu: (
|
||||
state,
|
||||
action: PayloadAction<
|
||||
FileManagerArgsBase<{
|
||||
open: boolean;
|
||||
pos: { x: number; y: number };
|
||||
type: string;
|
||||
targets: { [key: string]: FileResponse } | undefined;
|
||||
fmIndex: number;
|
||||
}>
|
||||
>,
|
||||
) => {
|
||||
const index = action.payload.index;
|
||||
const { open, pos, type, targets, fmIndex } = action.payload.value;
|
||||
state[index].contextMenuOpen = open;
|
||||
state[index].contextMenuPos = pos;
|
||||
state[index].contextMenuType = type;
|
||||
state[index].contextMenuTargets = targets;
|
||||
state[index].contextMenuTargetFm = fmIndex;
|
||||
},
|
||||
closeContextMenu: (state, action: PayloadAction<FileManagerArgsBase<any>>) => {
|
||||
const index = action.payload.index;
|
||||
state[index].contextMenuOpen = false;
|
||||
},
|
||||
setFileDeleteModal: (
|
||||
state,
|
||||
action: PayloadAction<FileManagerArgsBase<[boolean, FileResponse[] | undefined, string | undefined, boolean]>>,
|
||||
) => {
|
||||
const index = action.payload.index;
|
||||
const [open, selected, promiseId, loading] = action.payload.value;
|
||||
state[index].deleteFileModalOpen = open;
|
||||
state[index].deleteFileModalSelected = selected;
|
||||
state[index].deleteFileModalPromiseId = promiseId;
|
||||
state[index].deleteFileModalLoading = loading;
|
||||
},
|
||||
setFileDeleteModalLoading: (state, action: PayloadAction<FileManagerArgsBase<boolean>>) => {
|
||||
const index = action.payload.index;
|
||||
state[index].deleteFileModalLoading = action.payload.value;
|
||||
},
|
||||
setRenameFileModal: (
|
||||
state,
|
||||
action: PayloadAction<
|
||||
FileManagerArgsBase<{
|
||||
open: boolean;
|
||||
selected: FileResponse | undefined;
|
||||
promiseId: string | undefined;
|
||||
loading: boolean | undefined;
|
||||
error?: string;
|
||||
}>
|
||||
>,
|
||||
) => {
|
||||
const index = action.payload.index;
|
||||
state[index].renameFileModalOpen = action.payload.value.open;
|
||||
state[index].renameFileModalSelected = action.payload.value.selected;
|
||||
state[index].renameFileModalPromiseId = action.payload.value.promiseId;
|
||||
state[index].renameFileModalLoading = action.payload.value.loading;
|
||||
state[index].renameFileModalError = action.payload.value.error;
|
||||
},
|
||||
closeRenameFileModal: (state, action: PayloadAction<FileManagerArgsBase<any>>) => {
|
||||
const index = action.payload.index;
|
||||
state[index].renameFileModalOpen = false;
|
||||
},
|
||||
setRenameFileModalLoading: (state, action: PayloadAction<FileManagerArgsBase<boolean>>) => {
|
||||
const index = action.payload.index;
|
||||
state[index].renameFileModalLoading = action.payload.value;
|
||||
},
|
||||
setRenameFileModalError: (state, action: PayloadAction<FileManagerArgsBase<string | undefined>>) => {
|
||||
const index = action.payload.index;
|
||||
state[index].renameFileModalError = action.payload.value;
|
||||
},
|
||||
fileUpdated: (
|
||||
state,
|
||||
action: PayloadAction<
|
||||
FileManagerArgsBase<
|
||||
{
|
||||
oldPath: string;
|
||||
file: FileResponse;
|
||||
includeMetadata?: boolean;
|
||||
}[]
|
||||
>
|
||||
>,
|
||||
) => {
|
||||
for (const fmIndex of [0, 1]) {
|
||||
const fm = state[fmIndex];
|
||||
action.payload.value.forEach((v) => {
|
||||
const file = v.file;
|
||||
const oldPath = v.oldPath;
|
||||
const pathChanged = oldPath != file.path;
|
||||
let interestFields: {
|
||||
[key: string]: any;
|
||||
} = {
|
||||
name: file.name,
|
||||
updated_at: file.updated_at,
|
||||
size: file.size,
|
||||
created_at: file.created_at,
|
||||
id: file.id,
|
||||
path: file.path,
|
||||
shared: file.shared,
|
||||
primary_entity: file.primary_entity,
|
||||
};
|
||||
|
||||
if (v.includeMetadata) {
|
||||
interestFields = { ...interestFields, metadata: file.metadata };
|
||||
}
|
||||
|
||||
if (fm.list) {
|
||||
const list = fm.list.files;
|
||||
const idx = list.findIndex((f) => f.path == oldPath);
|
||||
if (idx >= 0) {
|
||||
list[idx] = {
|
||||
...list[idx],
|
||||
...interestFields,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (fm.selected[oldPath]) {
|
||||
fm.selected[file.path] = {
|
||||
...fm.selected[oldPath],
|
||||
...interestFields,
|
||||
};
|
||||
fm.selected[file.path].path = file.path;
|
||||
if (pathChanged) delete fm.selected[oldPath];
|
||||
}
|
||||
|
||||
if (fm.contextMenuTargets?.[oldPath]) {
|
||||
fm.contextMenuTargets[file.path] = {
|
||||
...fm.contextMenuTargets[oldPath],
|
||||
...interestFields,
|
||||
};
|
||||
fm.contextMenuTargets[file.path].path = file.path;
|
||||
if (pathChanged) delete fm.contextMenuTargets[oldPath];
|
||||
}
|
||||
|
||||
const treeCache = fm.tree[oldPath];
|
||||
if (treeCache && treeCache.file) {
|
||||
if (pathChanged) treeCache.children = [];
|
||||
treeCache.thumb = undefined;
|
||||
treeCache.file = {
|
||||
...treeCache.file,
|
||||
...interestFields,
|
||||
};
|
||||
}
|
||||
|
||||
fm.tree[file.path] = treeCache;
|
||||
if (pathChanged) delete fm.tree[oldPath];
|
||||
|
||||
// change path from parent's child
|
||||
if (pathChanged) {
|
||||
const parentPath = oldPath.substring(0, oldPath.lastIndexOf("/"));
|
||||
if (parentPath && fm.tree[parentPath]) {
|
||||
fm.tree[parentPath].children = state[fmIndex].tree[parentPath].children?.map((pa) =>
|
||||
pa == oldPath ? file.path : pa,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default fileManagerSlice.reducer;
|
||||
export const {
|
||||
removeThumbCache,
|
||||
clearMultiSelectHovered,
|
||||
setGalleryWidth,
|
||||
setListViewColumns,
|
||||
resetFileManager,
|
||||
fileUpdated,
|
||||
setRenameFileModalError,
|
||||
setRenameFileModal,
|
||||
closeRenameFileModal,
|
||||
setRenameFileModalLoading,
|
||||
removeTreeCache,
|
||||
setFileList,
|
||||
setFileDeleteModalLoading,
|
||||
setFileDeleteModal,
|
||||
closeContextMenu,
|
||||
setContextMenu,
|
||||
setMultiSelectHovered,
|
||||
setSelected,
|
||||
addSelected,
|
||||
removeSelected,
|
||||
clearSelected,
|
||||
setPage,
|
||||
setCapacity,
|
||||
clearSessionCache,
|
||||
setPathProps,
|
||||
appendTreeCache,
|
||||
appendListResponse,
|
||||
applyListResponse,
|
||||
setFmError,
|
||||
setFmLoading,
|
||||
setLayout,
|
||||
setShowThumb,
|
||||
setPageSize,
|
||||
setSortOption,
|
||||
setThumbCache,
|
||||
} = fileManagerSlice.actions;
|
||||
868
src/redux/globalStateSlice.ts
Executable file
868
src/redux/globalStateSlice.ts
Executable file
@@ -0,0 +1,868 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import {
|
||||
ConflictDetail,
|
||||
DirectLink,
|
||||
FileResponse,
|
||||
Share,
|
||||
StoragePolicy,
|
||||
Viewer,
|
||||
ViewerSession,
|
||||
} from "../api/explorer.ts";
|
||||
import { Response } from "../api/request.ts";
|
||||
import { User } from "../api/user.ts";
|
||||
import { SelectType } from "../component/Uploader/core";
|
||||
import SessionManager, { UserSettings } from "../session";
|
||||
|
||||
export interface DndState {
|
||||
dragging?: boolean;
|
||||
draggingWithSelected?: boolean;
|
||||
}
|
||||
|
||||
export interface ImageViewerState extends GeneralViewerState {
|
||||
index?: number;
|
||||
exts: string[];
|
||||
}
|
||||
|
||||
export interface ImageEditorState extends GeneralViewerState {}
|
||||
|
||||
export interface GeneralViewerState {
|
||||
open: boolean;
|
||||
file: FileResponse;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export interface ViewerSelectorState extends GeneralViewerState {
|
||||
viewers: Viewer[];
|
||||
entitySize: number;
|
||||
}
|
||||
|
||||
export interface WopiViewerState extends GeneralViewerState {
|
||||
src: string;
|
||||
session: ViewerSession;
|
||||
}
|
||||
|
||||
export interface DrawIOViewerState extends GeneralViewerState {
|
||||
host?: string;
|
||||
}
|
||||
|
||||
export interface CustomViewerState extends GeneralViewerState {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface MusicPlayerState {
|
||||
files: FileResponse[];
|
||||
startIndex: number;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export const CreateNewDialogType = {
|
||||
folder: "folder",
|
||||
file: "file",
|
||||
};
|
||||
|
||||
export interface UploadProgressTotal {
|
||||
processedSize: number;
|
||||
totalSize: number;
|
||||
}
|
||||
|
||||
export interface DialogSelectOption {
|
||||
name: string;
|
||||
description: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export interface GlobalStateSlice {
|
||||
loading: {
|
||||
headlessFrame: boolean;
|
||||
};
|
||||
preferredTheme?: string;
|
||||
drawerOpen: boolean;
|
||||
mobileDrawerOpen?: boolean;
|
||||
darkMode?: boolean;
|
||||
drawerWidth: number;
|
||||
userInfoCache: {
|
||||
[key: string]: User;
|
||||
};
|
||||
|
||||
// Others
|
||||
pinedGeneration: number;
|
||||
|
||||
// Dialogs
|
||||
// Aggregated error dialog
|
||||
aggregatedErrorDialogOpen?: boolean;
|
||||
aggregatedError?: Response<any>;
|
||||
aggregatedErrorFile?: {
|
||||
[key: string]: FileResponse;
|
||||
};
|
||||
|
||||
// Lock conflict dialog
|
||||
lockConflictDialogOpen?: boolean;
|
||||
lockConflictError?: Response<ConflictDetail[]>;
|
||||
lockConflictFile?: {
|
||||
[key: string]: FileResponse;
|
||||
};
|
||||
lockConflictPromiseId?: string;
|
||||
|
||||
// Confirmation dialog
|
||||
confirmDialogOpen?: boolean;
|
||||
confirmDialogMessage?: string;
|
||||
confirmPromiseId?: string;
|
||||
|
||||
// Pin file dialog
|
||||
pinFileDialogOpen?: boolean;
|
||||
pinFileUri?: string;
|
||||
|
||||
// path selection dialog
|
||||
pathSelectDialogOpen?: boolean;
|
||||
pathSelectDialogVariant?: string;
|
||||
pathSelectAllowedFs?: string[];
|
||||
pathSelectPromiseId?: string;
|
||||
pathSelectInitialPath?: string;
|
||||
|
||||
// Tags dialog
|
||||
tagsDialogOpen?: boolean;
|
||||
tagsDialogFile?: FileResponse[];
|
||||
|
||||
// Change icon dialog
|
||||
changeIconDialogOpen?: boolean;
|
||||
changeIconDialogFile?: FileResponse[];
|
||||
|
||||
// Share link dialog
|
||||
shareLinkDialogOpen?: boolean;
|
||||
shareLinkDialogFile?: FileResponse;
|
||||
shareLinkDialogShare?: Share;
|
||||
|
||||
// Version control dialog
|
||||
versionControlDialogOpen?: boolean;
|
||||
versionControlDialogFile?: FileResponse;
|
||||
versionControlHighlight?: string;
|
||||
|
||||
// Manage share link dialog
|
||||
manageShareDialogOpen?: boolean;
|
||||
manageShareDialogFile?: FileResponse;
|
||||
|
||||
// Stale version action dialog
|
||||
staleVersionDialogOpen?: boolean;
|
||||
staleVersionUri?: string;
|
||||
staleVersionPromiseId?: string;
|
||||
|
||||
// Save as dialog
|
||||
saveAsDialogOpen?: boolean;
|
||||
saveAsInitialName?: string;
|
||||
saveAsPromiseId?: string;
|
||||
|
||||
// Create new dialog
|
||||
createNewDialogOpen?: boolean;
|
||||
createNewDialogType?: string;
|
||||
createNewDialogDefault?: string;
|
||||
createNewPromiseId?: string;
|
||||
createNewDialogFmIndex?: number;
|
||||
|
||||
// Select option dialog
|
||||
selectOptionDialogOpen?: boolean;
|
||||
selectOptionDialogOptions?: DialogSelectOption[];
|
||||
selectOptionPromiseId?: string;
|
||||
selectOptionTitle?: string;
|
||||
|
||||
// Batch download log dialog
|
||||
batchDownloadLogDialogOpen?: boolean;
|
||||
batchDownloadLogDialogId?: string;
|
||||
batchDownloadLogDialogLogs?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
// Create archive dialog
|
||||
createArchiveDialogOpen?: boolean;
|
||||
createArchiveDialogFiles?: FileResponse[];
|
||||
|
||||
// Extract archive dialog
|
||||
extractArchiveDialogOpen?: boolean;
|
||||
extractArchiveDialogFile?: FileResponse;
|
||||
extractArchiveDialogMask?: string[];
|
||||
extractArchiveDialogEncoding?: string;
|
||||
|
||||
// Remote download dialog
|
||||
remoteDownloadDialogOpen?: boolean;
|
||||
remoteDownloadDialogFile?: FileResponse;
|
||||
|
||||
// List view column settings dialog
|
||||
listViewColumnSettingDialogOpen?: boolean;
|
||||
|
||||
// Direct Link result dialog
|
||||
directLinkDialogOpen?: boolean;
|
||||
directLinkRes?: DirectLink[];
|
||||
|
||||
// Direct Link management dialog
|
||||
directLinkManagementDialogOpen?: boolean;
|
||||
directLinkManagementDialogFile?: FileResponse;
|
||||
directLinkHighlight?: string;
|
||||
|
||||
// DnD
|
||||
dndState: DndState;
|
||||
|
||||
// Share info cache
|
||||
shareInfo: {
|
||||
[key: string]: Share;
|
||||
};
|
||||
|
||||
// Sidebar
|
||||
sidebarOpen?: boolean;
|
||||
sidebarTarget?: FileResponse | string;
|
||||
|
||||
// Viewers
|
||||
imageViewer?: ImageViewerState;
|
||||
imageEditor?: ImageEditorState;
|
||||
photopeaViewer?: GeneralViewerState;
|
||||
wopiViewer?: WopiViewerState;
|
||||
codeViewer?: GeneralViewerState;
|
||||
drawIOViewer?: DrawIOViewerState;
|
||||
markdownViewer?: GeneralViewerState;
|
||||
videoViewer?: GeneralViewerState;
|
||||
pdfViewer?: GeneralViewerState;
|
||||
customViewer?: CustomViewerState;
|
||||
epubViewer?: GeneralViewerState;
|
||||
musicPlayer?: MusicPlayerState;
|
||||
excalidrawViewer?: GeneralViewerState;
|
||||
archiveViewer?: GeneralViewerState;
|
||||
|
||||
// Viewer selector
|
||||
viewerSelector?: ViewerSelectorState;
|
||||
|
||||
// Uploader
|
||||
uploadFileSignal?: number;
|
||||
uploadFolderSignal?: number;
|
||||
uploadProgress?: UploadProgressTotal;
|
||||
uploadTaskCount?: number;
|
||||
uploadTaskListOpen?: boolean;
|
||||
uploadFromClipboardDialogOpen?: boolean;
|
||||
uploadRawFiles?: File[];
|
||||
uploadRawPromiseId?: string[];
|
||||
|
||||
policyOptionCache?: StoragePolicy[];
|
||||
|
||||
// Search popup
|
||||
searchPopupOpen?: boolean;
|
||||
|
||||
// Advance search
|
||||
advanceSearchOpen?: boolean;
|
||||
advanceSearchBasePath?: string;
|
||||
advanceSearchInitialNameCondition?: string[];
|
||||
|
||||
// Share README
|
||||
shareReadmeDetect?: number;
|
||||
shareReadmeOpen?: boolean;
|
||||
shareReadmeTarget?: FileResponse;
|
||||
}
|
||||
|
||||
let preferred_theme: string | undefined = undefined;
|
||||
try {
|
||||
preferred_theme = SessionManager.currentLogin().user.preferred_theme;
|
||||
} catch (e) {}
|
||||
|
||||
const initialState: GlobalStateSlice = {
|
||||
loading: {
|
||||
headlessFrame: false,
|
||||
},
|
||||
pinedGeneration: 0,
|
||||
darkMode: SessionManager.get(UserSettings.PreferredDarkMode),
|
||||
preferredTheme: preferred_theme,
|
||||
drawerOpen: true,
|
||||
drawerWidth: SessionManager.getWithFallback(UserSettings.DrawerWidth),
|
||||
userInfoCache: {},
|
||||
dndState: {},
|
||||
shareInfo: {},
|
||||
};
|
||||
|
||||
export const globalStateSlice = createSlice({
|
||||
name: "globalState",
|
||||
initialState,
|
||||
reducers: {
|
||||
setUploadRawFiles: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
files: File[];
|
||||
promiseId: string[];
|
||||
}>,
|
||||
) => {
|
||||
state.uploadRawFiles = action.payload.files ?? [];
|
||||
state.uploadRawPromiseId = action.payload.promiseId ?? [];
|
||||
},
|
||||
setShareReadmeDetect: (state, action: PayloadAction<boolean>) => {
|
||||
state.shareReadmeDetect = action.payload ? (state.shareReadmeDetect ?? 0) + 1 : 0;
|
||||
},
|
||||
setShareReadmeOpen: (state, action: PayloadAction<{ open: boolean; target?: FileResponse }>) => {
|
||||
state.shareReadmeOpen = action.payload.open;
|
||||
state.shareReadmeTarget = action.payload.target;
|
||||
},
|
||||
closeShareReadme: (state) => {
|
||||
state.shareReadmeOpen = false;
|
||||
},
|
||||
setDirectLinkManagementDialog: (
|
||||
state,
|
||||
action: PayloadAction<{ open: boolean; file?: FileResponse; highlight?: string }>,
|
||||
) => {
|
||||
state.directLinkManagementDialogOpen = action.payload.open;
|
||||
state.directLinkManagementDialogFile = action.payload.file;
|
||||
state.directLinkHighlight = action.payload.highlight;
|
||||
},
|
||||
closeDirectLinkManagementDialog: (state) => {
|
||||
state.directLinkManagementDialogOpen = false;
|
||||
state.directLinkManagementDialogFile = undefined;
|
||||
state.directLinkHighlight = undefined;
|
||||
},
|
||||
setMobileDrawerOpen: (state, action: PayloadAction<boolean>) => {
|
||||
state.mobileDrawerOpen = action.payload;
|
||||
},
|
||||
setDirectLinkDialog: (state, action: PayloadAction<{ open: boolean; res?: DirectLink[] }>) => {
|
||||
state.directLinkDialogOpen = action.payload.open;
|
||||
state.directLinkRes = action.payload.res;
|
||||
},
|
||||
closeDirectLinkDialog: (state) => {
|
||||
state.directLinkDialogOpen = false;
|
||||
},
|
||||
setListViewColumnSettingDialog: (state, action: PayloadAction<boolean>) => {
|
||||
state.listViewColumnSettingDialogOpen = action.payload;
|
||||
},
|
||||
setAdvanceSearch: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
open: boolean;
|
||||
basePath?: string;
|
||||
nameCondition?: string[];
|
||||
}>,
|
||||
) => {
|
||||
state.advanceSearchOpen = action.payload.open;
|
||||
state.advanceSearchBasePath = action.payload.basePath;
|
||||
state.advanceSearchInitialNameCondition = action.payload.nameCondition;
|
||||
},
|
||||
closeAdvanceSearch: (state) => {
|
||||
state.advanceSearchOpen = false;
|
||||
},
|
||||
setSearchPopup: (state, action: PayloadAction<boolean>) => {
|
||||
state.searchPopupOpen = action.payload;
|
||||
},
|
||||
setRemoteDownloadDialog: (state, action: PayloadAction<{ open: boolean; file?: FileResponse }>) => {
|
||||
state.remoteDownloadDialogOpen = action.payload.open;
|
||||
state.remoteDownloadDialogFile = action.payload.file;
|
||||
},
|
||||
closeRemoteDownloadDialog: (state) => {
|
||||
state.remoteDownloadDialogOpen = false;
|
||||
},
|
||||
setPolicyOptionCache: (state, action: PayloadAction<StoragePolicy[] | undefined>) => {
|
||||
state.policyOptionCache = action.payload;
|
||||
},
|
||||
resetDialogs: (state) => {
|
||||
state.aggregatedErrorDialogOpen = state.aggregatedErrorDialogOpen ? false : undefined;
|
||||
state.pathSelectDialogOpen = state.pathSelectDialogOpen ? false : undefined;
|
||||
state.tagsDialogOpen = state.tagsDialogOpen ? false : undefined;
|
||||
state.changeIconDialogOpen = state.changeIconDialogOpen ? false : undefined;
|
||||
state.shareLinkDialogOpen = state.shareLinkDialogOpen ? false : undefined;
|
||||
state.versionControlDialogOpen = state.versionControlDialogOpen ? false : undefined;
|
||||
state.manageShareDialogOpen = state.manageShareDialogOpen ? false : undefined;
|
||||
state.createNewDialogOpen = state.createNewDialogOpen ? false : undefined;
|
||||
state.selectOptionDialogOpen = state.selectOptionDialogOpen ? false : undefined;
|
||||
state.batchDownloadLogDialogOpen = state.batchDownloadLogDialogOpen ? false : undefined;
|
||||
state.createArchiveDialogOpen = state.createArchiveDialogOpen ? false : undefined;
|
||||
state.extractArchiveDialogOpen = state.extractArchiveDialogOpen ? false : undefined;
|
||||
|
||||
// reset all viewers
|
||||
state.imageViewer = undefined;
|
||||
state.imageEditor = undefined;
|
||||
state.photopeaViewer = undefined;
|
||||
state.wopiViewer = undefined;
|
||||
state.codeViewer = undefined;
|
||||
state.drawIOViewer = undefined;
|
||||
state.markdownViewer = undefined;
|
||||
state.videoViewer = undefined;
|
||||
state.pdfViewer = undefined;
|
||||
state.customViewer = undefined;
|
||||
state.epubViewer = undefined;
|
||||
state.excalidrawViewer = undefined;
|
||||
state.archiveViewer = undefined;
|
||||
},
|
||||
setExtractArchiveDialog: (
|
||||
state,
|
||||
action: PayloadAction<{ open: boolean; file?: FileResponse; mask?: string[]; encoding?: string }>,
|
||||
) => {
|
||||
state.extractArchiveDialogOpen = action.payload.open;
|
||||
state.extractArchiveDialogFile = action.payload.file;
|
||||
state.extractArchiveDialogMask = action.payload.mask;
|
||||
state.extractArchiveDialogEncoding = action.payload.encoding;
|
||||
},
|
||||
closeExtractArchiveDialog: (state) => {
|
||||
state.extractArchiveDialogOpen = false;
|
||||
state.extractArchiveDialogMask = undefined;
|
||||
state.extractArchiveDialogEncoding = undefined;
|
||||
},
|
||||
setCreateArchiveDialog: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
open: boolean;
|
||||
files: FileResponse[];
|
||||
}>,
|
||||
) => {
|
||||
state.createArchiveDialogOpen = action.payload.open;
|
||||
state.createArchiveDialogFiles = action.payload.files;
|
||||
},
|
||||
closeCreateArchiveDialog: (state) => {
|
||||
state.createArchiveDialogOpen = false;
|
||||
},
|
||||
setBatchDownloadLogDialog: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
open: boolean;
|
||||
id: string;
|
||||
}>,
|
||||
) => {
|
||||
state.batchDownloadLogDialogOpen = action.payload.open;
|
||||
state.batchDownloadLogDialogId = action.payload.id;
|
||||
},
|
||||
closeBatchDownloadLogDialog: (state) => {
|
||||
state.batchDownloadLogDialogOpen = false;
|
||||
},
|
||||
setBatchDownloadLog: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
id: string;
|
||||
logs: string;
|
||||
}>,
|
||||
) => {
|
||||
if (!state.batchDownloadLogDialogLogs) {
|
||||
state.batchDownloadLogDialogLogs = {};
|
||||
}
|
||||
state.batchDownloadLogDialogLogs[action.payload.id] = action.payload.logs;
|
||||
},
|
||||
setSelectOptionDialog: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
open: boolean;
|
||||
options?: DialogSelectOption[];
|
||||
promiseId: string;
|
||||
title?: string;
|
||||
}>,
|
||||
) => {
|
||||
state.selectOptionDialogOpen = action.payload.open;
|
||||
state.selectOptionDialogOptions = action.payload.options;
|
||||
state.selectOptionPromiseId = action.payload.promiseId;
|
||||
state.selectOptionTitle = action.payload.title;
|
||||
},
|
||||
closeSelectOptionDialog: (state) => {
|
||||
state.selectOptionDialogOpen = false;
|
||||
},
|
||||
setUploadFromClipboardDialog: (state, action: PayloadAction<boolean>) => {
|
||||
state.uploadFromClipboardDialogOpen = action.payload;
|
||||
},
|
||||
openUploadTaskList: (state) => {
|
||||
state.uploadTaskListOpen = true;
|
||||
},
|
||||
closeUploadTaskList: (state) => {
|
||||
state.uploadTaskListOpen = false;
|
||||
},
|
||||
setUploadProgress: (state, action: PayloadAction<{ progress: UploadProgressTotal; count: number }>) => {
|
||||
state.uploadProgress = action.payload.progress;
|
||||
state.uploadTaskCount = action.payload.count;
|
||||
},
|
||||
selectForUpload(state, action: PayloadAction<{ type: SelectType }>) {
|
||||
if (action.payload.type === SelectType.File) {
|
||||
state.uploadFileSignal = (state.uploadFileSignal ?? 0) + 1;
|
||||
} else {
|
||||
state.uploadFolderSignal = (state.uploadFolderSignal ?? 0) + 1;
|
||||
}
|
||||
},
|
||||
setCreateNewDialog: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
open: boolean;
|
||||
type?: string;
|
||||
default?: string;
|
||||
promiseId?: string;
|
||||
fmIndex: number;
|
||||
}>,
|
||||
) => {
|
||||
state.createNewDialogOpen = action.payload.open;
|
||||
state.createNewDialogType = action.payload.type;
|
||||
state.createNewDialogDefault = action.payload.default;
|
||||
state.createNewPromiseId = action.payload.promiseId;
|
||||
state.createNewDialogFmIndex = action.payload.fmIndex;
|
||||
},
|
||||
closeCreateNewDialog: (state) => {
|
||||
state.createNewDialogOpen = false;
|
||||
},
|
||||
setMusicPlayer: (state, action: PayloadAction<MusicPlayerState>) => {
|
||||
state.musicPlayer = action.payload;
|
||||
},
|
||||
closeMusicPlayer: (state) => {
|
||||
state.musicPlayer = undefined;
|
||||
},
|
||||
setEpubViewer: (state, action: PayloadAction<GeneralViewerState>) => {
|
||||
state.epubViewer = action.payload;
|
||||
},
|
||||
closeEpubViewer: (state) => {
|
||||
state.epubViewer && (state.epubViewer.open = false);
|
||||
},
|
||||
setCustomViewer: (state, action: PayloadAction<CustomViewerState>) => {
|
||||
state.customViewer = action.payload;
|
||||
},
|
||||
closeCustomViewer: (state) => {
|
||||
state.customViewer && (state.customViewer.open = false);
|
||||
},
|
||||
setPdfViewer: (state, action: PayloadAction<GeneralViewerState>) => {
|
||||
state.pdfViewer = action.payload;
|
||||
},
|
||||
closePdfViewer: (state) => {
|
||||
state.pdfViewer && (state.pdfViewer.open = false);
|
||||
},
|
||||
setVideoViewer: (state, action: PayloadAction<GeneralViewerState>) => {
|
||||
state.videoViewer = action.payload;
|
||||
},
|
||||
closeVideoViewer: (state) => {
|
||||
state.videoViewer && (state.videoViewer.open = false);
|
||||
},
|
||||
setMarkdownViewer: (state, action: PayloadAction<GeneralViewerState>) => {
|
||||
state.markdownViewer = action.payload;
|
||||
},
|
||||
closeMarkdownViewer: (state) => {
|
||||
state.markdownViewer && (state.markdownViewer.open = false);
|
||||
},
|
||||
setExcalidrawViewer: (state, action: PayloadAction<GeneralViewerState>) => {
|
||||
state.excalidrawViewer = action.payload;
|
||||
},
|
||||
closeExcalidrawViewer: (state) => {
|
||||
state.excalidrawViewer && (state.excalidrawViewer.open = false);
|
||||
},
|
||||
setArchiveViewer: (state, action: PayloadAction<GeneralViewerState>) => {
|
||||
state.archiveViewer = action.payload;
|
||||
},
|
||||
closeArchiveViewer: (state) => {
|
||||
state.archiveViewer && (state.archiveViewer.open = false);
|
||||
},
|
||||
addShareInfo: (state, action: PayloadAction<{ info: Share; id: string }>) => {
|
||||
state.shareInfo[action.payload.id] = action.payload.info;
|
||||
},
|
||||
setHeadlessFrameLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.loading.headlessFrame = action.payload;
|
||||
},
|
||||
setPreferredTheme: (state, action: PayloadAction<string>) => {
|
||||
state.preferredTheme = action.payload;
|
||||
},
|
||||
setDrawerOpen: (state, action: PayloadAction<boolean>) => {
|
||||
state.drawerOpen = action.payload;
|
||||
},
|
||||
setDrawerWidth: (state, action: PayloadAction<number>) => {
|
||||
state.drawerWidth = action.payload;
|
||||
},
|
||||
setDarkMode: (state, action: PayloadAction<boolean | undefined>) => {
|
||||
state.darkMode = action.payload;
|
||||
},
|
||||
setUserInfoCache: (state, action: PayloadAction<[string, User]>) => {
|
||||
state.userInfoCache[action.payload[0]] = action.payload[1];
|
||||
},
|
||||
setAggregatedErrorDialog: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
open: boolean;
|
||||
error?: Response<any>;
|
||||
files?: { [key: string]: FileResponse };
|
||||
}>,
|
||||
) => {
|
||||
state.aggregatedErrorDialogOpen = action.payload.open;
|
||||
state.aggregatedError = action.payload.error;
|
||||
state.aggregatedErrorFile = action.payload.files;
|
||||
},
|
||||
closeAggregatedErrorDialog: (state) => {
|
||||
state.aggregatedErrorDialogOpen = false;
|
||||
},
|
||||
setLockConflictDialog: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
open: boolean;
|
||||
error?: Response<ConflictDetail[]>;
|
||||
files?: { [key: string]: FileResponse };
|
||||
promiseId?: string;
|
||||
}>,
|
||||
) => {
|
||||
state.lockConflictDialogOpen = action.payload.open;
|
||||
state.lockConflictError = action.payload.error;
|
||||
state.lockConflictFile = action.payload.files;
|
||||
state.lockConflictPromiseId = action.payload.promiseId;
|
||||
},
|
||||
closeLockConflictDialog: (state) => {
|
||||
state.lockConflictDialogOpen = false;
|
||||
},
|
||||
updateLockConflicts: (state, action: PayloadAction<Response<ConflictDetail[]>>) => {
|
||||
state.lockConflictError = action.payload;
|
||||
},
|
||||
setConfirmDialog: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
open: boolean;
|
||||
message?: string;
|
||||
promiseId?: string;
|
||||
}>,
|
||||
) => {
|
||||
state.confirmDialogOpen = action.payload.open;
|
||||
state.confirmDialogMessage = action.payload.message;
|
||||
state.confirmPromiseId = action.payload.promiseId;
|
||||
},
|
||||
closeConfirmDialog: (state) => {
|
||||
state.confirmDialogOpen = false;
|
||||
},
|
||||
increasePinedGeneration: (state) => {
|
||||
state.pinedGeneration += 1;
|
||||
},
|
||||
setPinFileDialog: (state, action: PayloadAction<{ open: boolean; uri?: string }>) => {
|
||||
state.pinFileDialogOpen = action.payload.open;
|
||||
state.pinFileUri = action.payload.uri;
|
||||
},
|
||||
closePinFileDialog: (state) => {
|
||||
state.pinFileDialogOpen = false;
|
||||
},
|
||||
setDragging: (state, action: PayloadAction<DndState>) => {
|
||||
state.dndState.dragging = action.payload.dragging;
|
||||
state.dndState.draggingWithSelected = action.payload.draggingWithSelected;
|
||||
},
|
||||
setPathSelectionDialog: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
open: boolean;
|
||||
variant: string;
|
||||
promiseId: string;
|
||||
initialPath?: string;
|
||||
}>,
|
||||
) => {
|
||||
state.pathSelectDialogOpen = action.payload.open;
|
||||
state.pathSelectDialogVariant = action.payload.variant;
|
||||
state.pathSelectPromiseId = action.payload.promiseId;
|
||||
state.pathSelectInitialPath = action.payload.initialPath;
|
||||
},
|
||||
closePathSelectionDialog: (state) => {
|
||||
state.pathSelectDialogOpen = false;
|
||||
},
|
||||
setTagsDialog: (state, action: PayloadAction<{ open: boolean; file?: FileResponse[] }>) => {
|
||||
state.tagsDialogOpen = action.payload.open;
|
||||
state.tagsDialogFile = action.payload.file;
|
||||
},
|
||||
closeTagsDialog: (state) => {
|
||||
state.tagsDialogOpen = false;
|
||||
},
|
||||
setChangeIconDialog: (state, action: PayloadAction<{ open: boolean; file?: FileResponse[] }>) => {
|
||||
state.changeIconDialogOpen = action.payload.open;
|
||||
state.changeIconDialogFile = action.payload.file;
|
||||
},
|
||||
closeChangeIconDialog: (state) => {
|
||||
state.changeIconDialogOpen = false;
|
||||
},
|
||||
setShareLinkDialog: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
open: boolean;
|
||||
file?: FileResponse;
|
||||
share?: Share;
|
||||
}>,
|
||||
) => {
|
||||
state.shareLinkDialogOpen = action.payload.open;
|
||||
state.shareLinkDialogFile = action.payload.file;
|
||||
state.shareLinkDialogShare = action.payload.share;
|
||||
},
|
||||
closeShareLinkDialog: (state) => {
|
||||
state.shareLinkDialogOpen = false;
|
||||
},
|
||||
setSidebar: (state, action: PayloadAction<{ open: boolean; target?: FileResponse | string }>) => {
|
||||
state.sidebarOpen = action.payload.open;
|
||||
state.sidebarTarget = action.payload.target;
|
||||
},
|
||||
closeSidebar: (state) => {
|
||||
state.sidebarOpen = false;
|
||||
},
|
||||
setVersionControlDialog: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
open: boolean;
|
||||
file?: FileResponse;
|
||||
highlight?: string;
|
||||
}>,
|
||||
) => {
|
||||
state.versionControlDialogOpen = action.payload.open;
|
||||
state.versionControlDialogFile = action.payload.file;
|
||||
state.versionControlHighlight = action.payload.highlight;
|
||||
},
|
||||
closeVersionControlDialog: (state) => {
|
||||
state.versionControlDialogOpen = false;
|
||||
},
|
||||
setManageShareDialog: (state, action: PayloadAction<{ open: boolean; file: FileResponse }>) => {
|
||||
state.manageShareDialogOpen = action.payload.open;
|
||||
state.manageShareDialogFile = action.payload.file;
|
||||
},
|
||||
closeManageShareDialog: (state) => {
|
||||
state.manageShareDialogOpen = false;
|
||||
},
|
||||
setImageViewer: (state, action: PayloadAction<ImageViewerState>) => {
|
||||
state.imageViewer = action.payload;
|
||||
},
|
||||
closeImageViewer: (state) => {
|
||||
state.imageViewer = undefined;
|
||||
},
|
||||
setImageEditor: (state, action: PayloadAction<ImageEditorState>) => {
|
||||
state.imageEditor = action.payload;
|
||||
},
|
||||
closeImageEditor: (state) => {
|
||||
state.imageEditor = undefined;
|
||||
},
|
||||
setStaleVersionDialog: (state, action: PayloadAction<{ open: boolean; promiseId: string; uri: string }>) => {
|
||||
state.staleVersionDialogOpen = action.payload.open;
|
||||
state.staleVersionPromiseId = action.payload.promiseId;
|
||||
state.staleVersionUri = action.payload.uri;
|
||||
},
|
||||
closeStaleVersionDialog: (state) => {
|
||||
state.staleVersionDialogOpen = false;
|
||||
},
|
||||
setSaveAsDialog: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
open: boolean;
|
||||
name?: string;
|
||||
promiseId: string;
|
||||
}>,
|
||||
) => {
|
||||
state.saveAsDialogOpen = action.payload.open;
|
||||
state.saveAsInitialName = action.payload.name;
|
||||
state.saveAsPromiseId = action.payload.promiseId;
|
||||
},
|
||||
closeSaveAsDialog: (state) => {
|
||||
state.saveAsDialogOpen = false;
|
||||
},
|
||||
setPhotopeaViewer: (state, action: PayloadAction<GeneralViewerState>) => {
|
||||
state.photopeaViewer = action.payload;
|
||||
},
|
||||
closePhotopeaViewer: (state) => {
|
||||
state.photopeaViewer && (state.photopeaViewer.open = false);
|
||||
},
|
||||
setViewerSelector: (state, action: PayloadAction<ViewerSelectorState>) => {
|
||||
state.viewerSelector = action.payload;
|
||||
},
|
||||
closeViewerSelector: (state) => {
|
||||
state.viewerSelector && (state.viewerSelector.open = false);
|
||||
},
|
||||
setWopiViewer: (state, action: PayloadAction<WopiViewerState>) => {
|
||||
state.wopiViewer = action.payload;
|
||||
},
|
||||
closeWopiViewer: (state) => {
|
||||
state.wopiViewer && (state.wopiViewer.open = false);
|
||||
},
|
||||
setCodeViewer: (state, action: PayloadAction<GeneralViewerState>) => {
|
||||
state.codeViewer = action.payload;
|
||||
},
|
||||
closeCodeViewer: (state) => {
|
||||
state.codeViewer && (state.codeViewer.open = false);
|
||||
},
|
||||
setDrawIOViewer: (state, action: PayloadAction<DrawIOViewerState>) => {
|
||||
state.drawIOViewer = action.payload;
|
||||
},
|
||||
closeDrawIOViewer: (state) => {
|
||||
state.drawIOViewer && (state.drawIOViewer.open = false);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default globalStateSlice.reducer;
|
||||
export const {
|
||||
setArchiveViewer,
|
||||
closeArchiveViewer,
|
||||
setUploadRawFiles,
|
||||
setMobileDrawerOpen,
|
||||
setDirectLinkDialog,
|
||||
closeDirectLinkDialog,
|
||||
setListViewColumnSettingDialog,
|
||||
closeAdvanceSearch,
|
||||
setAdvanceSearch,
|
||||
setRemoteDownloadDialog,
|
||||
closeRemoteDownloadDialog,
|
||||
setExtractArchiveDialog,
|
||||
closeExtractArchiveDialog,
|
||||
setCreateArchiveDialog,
|
||||
closeCreateArchiveDialog,
|
||||
setBatchDownloadLogDialog,
|
||||
closeBatchDownloadLogDialog,
|
||||
setBatchDownloadLog,
|
||||
setSelectOptionDialog,
|
||||
closeSelectOptionDialog,
|
||||
setUploadFromClipboardDialog,
|
||||
openUploadTaskList,
|
||||
closeUploadTaskList,
|
||||
setUploadProgress,
|
||||
selectForUpload,
|
||||
setCreateNewDialog,
|
||||
closeCreateNewDialog,
|
||||
setMusicPlayer,
|
||||
closeMusicPlayer,
|
||||
setEpubViewer,
|
||||
closeEpubViewer,
|
||||
setCustomViewer,
|
||||
closeCustomViewer,
|
||||
setPdfViewer,
|
||||
closePdfViewer,
|
||||
setVideoViewer,
|
||||
closeVideoViewer,
|
||||
setMarkdownViewer,
|
||||
closeMarkdownViewer,
|
||||
setDrawIOViewer,
|
||||
closeDrawIOViewer,
|
||||
setCodeViewer,
|
||||
closeCodeViewer,
|
||||
setWopiViewer,
|
||||
closeWopiViewer,
|
||||
setViewerSelector,
|
||||
closeViewerSelector,
|
||||
setPhotopeaViewer,
|
||||
closePhotopeaViewer,
|
||||
setStaleVersionDialog,
|
||||
closeStaleVersionDialog,
|
||||
setImageViewer,
|
||||
closeImageViewer,
|
||||
setManageShareDialog,
|
||||
closeManageShareDialog,
|
||||
setVersionControlDialog,
|
||||
closeVersionControlDialog,
|
||||
closeSidebar,
|
||||
setSidebar,
|
||||
addShareInfo,
|
||||
setShareLinkDialog,
|
||||
closeShareLinkDialog,
|
||||
setChangeIconDialog,
|
||||
closeChangeIconDialog,
|
||||
setTagsDialog,
|
||||
closeTagsDialog,
|
||||
setPathSelectionDialog,
|
||||
closePathSelectionDialog,
|
||||
setDragging,
|
||||
setPinFileDialog,
|
||||
closePinFileDialog,
|
||||
increasePinedGeneration,
|
||||
setConfirmDialog,
|
||||
closeConfirmDialog,
|
||||
updateLockConflicts,
|
||||
closeLockConflictDialog,
|
||||
setLockConflictDialog,
|
||||
closeAggregatedErrorDialog,
|
||||
setAggregatedErrorDialog,
|
||||
setDarkMode,
|
||||
setDrawerOpen,
|
||||
setDrawerWidth,
|
||||
setHeadlessFrameLoading,
|
||||
setPreferredTheme,
|
||||
setUserInfoCache,
|
||||
setImageEditor,
|
||||
closeImageEditor,
|
||||
setSaveAsDialog,
|
||||
closeSaveAsDialog,
|
||||
resetDialogs,
|
||||
setPolicyOptionCache,
|
||||
setSearchPopup,
|
||||
setExcalidrawViewer,
|
||||
closeExcalidrawViewer,
|
||||
setDirectLinkManagementDialog,
|
||||
closeDirectLinkManagementDialog,
|
||||
setShareReadmeDetect,
|
||||
closeShareReadme,
|
||||
setShareReadmeOpen,
|
||||
} = globalStateSlice.actions;
|
||||
5
src/redux/hooks.ts
Executable file
5
src/redux/hooks.ts
Executable file
@@ -0,0 +1,5 @@
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||
import { AppDispatch, RootState } from "./store.ts";
|
||||
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
182
src/redux/siteConfigSlice.ts
Executable file
182
src/redux/siteConfigSlice.ts
Executable file
@@ -0,0 +1,182 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { Viewer, ViewerPlatform } from "../api/explorer.ts";
|
||||
import { SiteConfig } from "../api/site.ts";
|
||||
import { ExpandedIconSettings, FileTypeIconSetting } from "../component/FileManager/Explorer/FileTypeIcon.tsx";
|
||||
import SessionManager from "../session/index.ts";
|
||||
import Boolset from "../util/boolset.ts";
|
||||
import { ExpandedViewerSetting } from "./thunks/viewer.ts";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
subTitle: string;
|
||||
}
|
||||
}
|
||||
|
||||
export enum ConfigLoadState {
|
||||
NotLoaded,
|
||||
CacheHit,
|
||||
Loaded,
|
||||
}
|
||||
|
||||
export interface SiteConfigSlice {
|
||||
[key: string]: {
|
||||
loaded: ConfigLoadState;
|
||||
config: SiteConfig;
|
||||
typed?: any;
|
||||
};
|
||||
}
|
||||
|
||||
const initialState: SiteConfigSlice = {
|
||||
login: {
|
||||
loaded: ConfigLoadState.NotLoaded,
|
||||
config: {},
|
||||
},
|
||||
basic: {
|
||||
loaded: ConfigLoadState.NotLoaded,
|
||||
config: {},
|
||||
},
|
||||
explorer: {
|
||||
loaded: ConfigLoadState.NotLoaded,
|
||||
config: {},
|
||||
},
|
||||
emojis: {
|
||||
loaded: ConfigLoadState.NotLoaded,
|
||||
config: {},
|
||||
},
|
||||
app: {
|
||||
loaded: ConfigLoadState.NotLoaded,
|
||||
config: {},
|
||||
},
|
||||
thumb: {
|
||||
loaded: ConfigLoadState.NotLoaded,
|
||||
config: {},
|
||||
},
|
||||
};
|
||||
|
||||
export let Viewers: ExpandedViewerSetting = {};
|
||||
export let ViewersByID: { [key: string]: Viewer } = {};
|
||||
|
||||
const preProcessors: {
|
||||
[key: string]: (config: SiteConfig) => any;
|
||||
} = {
|
||||
explorer: (config: SiteConfig) => {
|
||||
let icons: ExpandedIconSettings = {};
|
||||
try {
|
||||
const iconSettings = <FileTypeIconSetting[]>JSON.parse(config.icons ?? "[]");
|
||||
iconSettings.forEach((item) => {
|
||||
item.exts.forEach((ext) => {
|
||||
icons[ext] = { ...item, exts: [] };
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn("Failed to parse icons config", e);
|
||||
}
|
||||
|
||||
Viewers = {};
|
||||
ViewersByID = {};
|
||||
const isMobile = window.matchMedia("(max-width: 768px)").matches;
|
||||
config.file_viewers?.forEach((group) => {
|
||||
group.viewers.forEach((viewer) => {
|
||||
if (viewer.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewer.required_group_permission) {
|
||||
const group = SessionManager.currentUserGroup();
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupBs = new Boolset(group.permission);
|
||||
if (viewer.required_group_permission.some((p) => !groupBs.enabled(p))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const platform = viewer.platform || ViewerPlatform.all;
|
||||
if (platform !== ViewerPlatform.all && platform !== (isMobile ? ViewerPlatform.mobile : ViewerPlatform.pc)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ViewersByID[viewer.id] = viewer;
|
||||
const simplified: Viewer = viewer;
|
||||
viewer.exts.forEach((ext) => {
|
||||
if (Viewers[ext] === undefined) {
|
||||
Viewers[ext] = [];
|
||||
}
|
||||
Viewers[ext].push(simplified);
|
||||
});
|
||||
});
|
||||
});
|
||||
return { icons };
|
||||
},
|
||||
};
|
||||
|
||||
const loadSiteConfigCache = (initial: SiteConfigSlice): SiteConfigSlice => {
|
||||
Object.entries(initial).forEach(([key, _value]) => {
|
||||
const cacheContent = localStorage.getItem(`siteConfigCache_${key}`);
|
||||
if (cacheContent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const configCache = JSON.parse(cacheContent) as SiteConfig;
|
||||
initial[key].loaded = ConfigLoadState.CacheHit;
|
||||
initial[key].config = configCache;
|
||||
if (preProcessors[key]) {
|
||||
initial[key].typed = preProcessors[key](configCache);
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// // 检查是否有path参数
|
||||
// const url = new URL(window.location.href);
|
||||
// const c = url.searchParams.get("path");
|
||||
// rawStore.navigator.path = c === null ? "/" : c;
|
||||
// // 初始化用户个性配置
|
||||
// rawStore.siteConfig = initUserConfig(rawStore.siteConfig);
|
||||
});
|
||||
|
||||
// 更改站点标题
|
||||
document.title = initial["basic"].config.title ?? "";
|
||||
|
||||
return initial;
|
||||
};
|
||||
|
||||
export const siteConfigSlice = createSlice({
|
||||
name: "siteConfig",
|
||||
initialState: loadSiteConfigCache(initialState),
|
||||
// The `reducers` field lets us define reducers and generate associated actions
|
||||
reducers: {
|
||||
applySetting: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
section: string;
|
||||
config: SiteConfig;
|
||||
}>,
|
||||
) => {
|
||||
state[action.payload.section].loaded = ConfigLoadState.Loaded;
|
||||
state[action.payload.section].config = action.payload.config;
|
||||
if (preProcessors[action.payload.section]) {
|
||||
state[action.payload.section].typed = preProcessors[action.payload.section](action.payload.config);
|
||||
}
|
||||
},
|
||||
},
|
||||
// // The `extraReducers` field lets the slice handle actions defined elsewhere,
|
||||
// // including actions generated by createAsyncThunk or in other slices.
|
||||
// extraReducers: (builder) => {
|
||||
// builder
|
||||
// .addCase(incrementAsync.pending, (state) => {
|
||||
// state.status = "loading";
|
||||
// })
|
||||
// .addCase(incrementAsync.fulfilled, (state, action) => {
|
||||
// state.status = "idle";
|
||||
// state.value += action.payload;
|
||||
// })
|
||||
// .addCase(incrementAsync.rejected, (state) => {
|
||||
// state.status = "failed";
|
||||
// });
|
||||
// },
|
||||
});
|
||||
|
||||
export default siteConfigSlice.reducer;
|
||||
export const { applySetting } = siteConfigSlice.actions;
|
||||
21
src/redux/store.ts
Executable file
21
src/redux/store.ts
Executable file
@@ -0,0 +1,21 @@
|
||||
import { Action, configureStore } from "@reduxjs/toolkit";
|
||||
import { ThunkAction } from "redux-thunk";
|
||||
import { updateSiteConfig } from "./thunks/site.ts";
|
||||
import siteConfigSliceReducer from "./siteConfigSlice";
|
||||
import globalStateSliceReducer from "./globalStateSlice";
|
||||
import fileManagerSliceReducer from "./fileManagerSlice.ts";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
siteConfig: siteConfigSliceReducer,
|
||||
globalState: globalStateSliceReducer,
|
||||
fileManager: fileManagerSliceReducer,
|
||||
},
|
||||
devTools: process.env.NODE_ENV !== "production",
|
||||
});
|
||||
|
||||
store.dispatch(updateSiteConfig());
|
||||
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppThunk<ReturnType = void> = ThunkAction<ReturnType, RootState, unknown, Action<string>>;
|
||||
267
src/redux/thunks/dialog.ts
Executable file
267
src/redux/thunks/dialog.ts
Executable file
@@ -0,0 +1,267 @@
|
||||
import { ConflictDetail, FileResponse } from "../../api/explorer.ts";
|
||||
import { Response } from "../../api/request.ts";
|
||||
import { DeleteOption } from "../../component/FileManager/Dialogs/DeleteConfirmation.tsx";
|
||||
import { setFileDeleteModal, setRenameFileModal } from "../fileManagerSlice.ts";
|
||||
import {
|
||||
DialogSelectOption,
|
||||
setAggregatedErrorDialog,
|
||||
setConfirmDialog,
|
||||
setCreateNewDialog,
|
||||
setLockConflictDialog,
|
||||
setPathSelectionDialog,
|
||||
setSaveAsDialog,
|
||||
setSelectOptionDialog,
|
||||
setStaleVersionDialog,
|
||||
} from "../globalStateSlice.ts";
|
||||
import { AppThunk } from "../store.ts";
|
||||
|
||||
export const promiseId = () => new Date().getTime().toString();
|
||||
export const deleteDialogPromisePool: {
|
||||
[key: string]: {
|
||||
resolve: (value: DeleteOption | PromiseLike<DeleteOption>) => void;
|
||||
reject: (reason?: any) => void;
|
||||
};
|
||||
} = {};
|
||||
|
||||
export const generalDialogPromisePool: {
|
||||
[key: string]: {
|
||||
resolve: (value: void | PromiseLike<void>) => void;
|
||||
reject: (reason?: any) => void;
|
||||
};
|
||||
} = {};
|
||||
|
||||
export const renameDialogPromisePool: {
|
||||
[key: string]: {
|
||||
resolve: (value: string | PromiseLike<string>) => void;
|
||||
reject: (reason?: any) => void;
|
||||
};
|
||||
} = {};
|
||||
|
||||
export const pathSelectionDialogPromisePool: {
|
||||
[key: string]: {
|
||||
resolve: (value: string | PromiseLike<string>) => void;
|
||||
reject: (reason?: any) => void;
|
||||
};
|
||||
} = {};
|
||||
|
||||
export const createNewDialogPromisePool: {
|
||||
[key: string]: {
|
||||
resolve: (value: FileResponse | PromiseLike<FileResponse>) => void;
|
||||
reject: (reason?: any) => void;
|
||||
};
|
||||
} = {};
|
||||
|
||||
export const selectOptionDialogPromisePool: {
|
||||
[key: string]: {
|
||||
resolve: (value: any | PromiseLike<any>) => void;
|
||||
reject: (reason?: any) => void;
|
||||
};
|
||||
} = {};
|
||||
|
||||
export interface StaleVersionAction {
|
||||
overwrite?: boolean;
|
||||
saveAs?: string;
|
||||
}
|
||||
|
||||
export const staleVersionDialogPromisePool: {
|
||||
[key: string]: {
|
||||
resolve: (value: StaleVersionAction | PromiseLike<StaleVersionAction>) => void;
|
||||
reject: (reason?: any) => void;
|
||||
};
|
||||
} = {};
|
||||
|
||||
export interface SaveAsAction {
|
||||
uri: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const saveAsDialogPromisePool: {
|
||||
[key: string]: {
|
||||
resolve: (value: SaveAsAction | PromiseLike<SaveAsAction>) => void;
|
||||
reject: (reason?: any) => void;
|
||||
};
|
||||
} = {};
|
||||
|
||||
export function deleteConfirmation(index: number, files: FileResponse[]): AppThunk<Promise<DeleteOption>> {
|
||||
return async (dispatch, _getState) => {
|
||||
const id = promiseId();
|
||||
return new Promise<DeleteOption>((resolve, reject) => {
|
||||
deleteDialogPromisePool[id] = { resolve, reject };
|
||||
dispatch(setFileDeleteModal({ index, value: [true, files, id, false] }));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function renameForm(index: number, file: FileResponse): AppThunk<Promise<string>> {
|
||||
return async (dispatch, _getState) => {
|
||||
const id = promiseId();
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
renameDialogPromisePool[id] = { resolve, reject };
|
||||
dispatch(
|
||||
setRenameFileModal({
|
||||
index,
|
||||
value: {
|
||||
open: true,
|
||||
selected: file,
|
||||
promiseId: id,
|
||||
loading: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function showAggregatedErrorDialog(error: Response<any>): AppThunk {
|
||||
return (dispatch, getState) => {
|
||||
const tree = getState().fileManager[0].tree;
|
||||
const files: {
|
||||
[key: string]: FileResponse;
|
||||
} = {};
|
||||
Object.keys(error.aggregated_error ?? {}).forEach((path) => {
|
||||
const cached = tree[path]?.file;
|
||||
if (cached) {
|
||||
files[path] = cached;
|
||||
}
|
||||
});
|
||||
dispatch(
|
||||
setAggregatedErrorDialog({
|
||||
open: true,
|
||||
error: error,
|
||||
files: files,
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function openLockConflictDialog(error: Response<ConflictDetail[]>): AppThunk<Promise<void>> {
|
||||
return (dispatch, getState) => {
|
||||
const tree = getState().fileManager[0].tree;
|
||||
const files: {
|
||||
[key: string]: FileResponse;
|
||||
} = {};
|
||||
error.data.forEach((conflict) => {
|
||||
if (!conflict.path) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cached = tree[conflict.path]?.file;
|
||||
if (cached) {
|
||||
files[conflict.path] = cached;
|
||||
}
|
||||
});
|
||||
|
||||
const id = promiseId();
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
generalDialogPromisePool[id] = { resolve, reject };
|
||||
dispatch(
|
||||
setLockConflictDialog({
|
||||
open: true,
|
||||
error: error,
|
||||
files: files,
|
||||
promiseId: id,
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function confirmOperation(message: string): AppThunk<Promise<void>> {
|
||||
return (dispatch) => {
|
||||
const id = promiseId();
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
generalDialogPromisePool[id] = { resolve, reject };
|
||||
dispatch(
|
||||
setConfirmDialog({
|
||||
open: true,
|
||||
message: message,
|
||||
promiseId: id,
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function selectPath(variant: string, initialPath?: string): AppThunk<Promise<string>> {
|
||||
return (dispatch) => {
|
||||
const id = promiseId();
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
pathSelectionDialogPromisePool[id] = { resolve, reject };
|
||||
dispatch(
|
||||
setPathSelectionDialog({
|
||||
open: true,
|
||||
variant: variant,
|
||||
promiseId: id,
|
||||
initialPath: initialPath,
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function askStaleVersionAction(uri: string): AppThunk<Promise<StaleVersionAction>> {
|
||||
return async (dispatch, _getState) => {
|
||||
const id = promiseId();
|
||||
return new Promise<StaleVersionAction>((resolve, reject) => {
|
||||
staleVersionDialogPromisePool[id] = { resolve, reject };
|
||||
dispatch(
|
||||
setStaleVersionDialog({
|
||||
open: true,
|
||||
uri,
|
||||
promiseId: id,
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function askSaveAs(name: string): AppThunk<Promise<SaveAsAction>> {
|
||||
return async (dispatch, _getState) => {
|
||||
const id = promiseId();
|
||||
return new Promise<SaveAsAction>((resolve, reject) => {
|
||||
saveAsDialogPromisePool[id] = { resolve, reject };
|
||||
dispatch(
|
||||
setSaveAsDialog({
|
||||
open: true,
|
||||
name,
|
||||
promiseId: id,
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function requestCreateNew(fmIndex: number, type: string, defaultName?: string): AppThunk<Promise<FileResponse>> {
|
||||
return async (dispatch, _getState) => {
|
||||
const id = promiseId();
|
||||
return new Promise<FileResponse>((resolve, reject) => {
|
||||
createNewDialogPromisePool[id] = { resolve, reject };
|
||||
dispatch(
|
||||
setCreateNewDialog({
|
||||
open: true,
|
||||
type,
|
||||
default: defaultName,
|
||||
promiseId: id,
|
||||
fmIndex,
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function selectOption(options: DialogSelectOption[], title: string): AppThunk<Promise<any> | Promise<void>> {
|
||||
return async (dispatch) => {
|
||||
const id = promiseId();
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
selectOptionDialogPromisePool[id] = { resolve, reject };
|
||||
dispatch(
|
||||
setSelectOptionDialog({
|
||||
open: true,
|
||||
title,
|
||||
options,
|
||||
promiseId: id,
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
449
src/redux/thunks/download.ts
Executable file
449
src/redux/thunks/download.ts
Executable file
@@ -0,0 +1,449 @@
|
||||
import dayjs from "dayjs";
|
||||
import i18next from "i18next";
|
||||
import { closeSnackbar, enqueueSnackbar } from "notistack";
|
||||
import streamSaver from "streamsaver";
|
||||
import { getFileEntityUrl } from "../../api/api.ts";
|
||||
import { FileResponse, FileType, Metadata } from "../../api/explorer.ts";
|
||||
import { GroupPermission } from "../../api/user.ts";
|
||||
import { ViewDownloadLogAction } from "../../component/Common/Snackbar/snackbar.tsx";
|
||||
import SessionManager from "../../session";
|
||||
import { getFileLinkedUri } from "../../util";
|
||||
import Boolset from "../../util/boolset.ts";
|
||||
import { formatLocalTime } from "../../util/datetime.ts";
|
||||
import {
|
||||
getFileSystemDirectoryPaths,
|
||||
saveFileToFileSystemDirectory,
|
||||
verifyFileSystemRWPermission,
|
||||
} from "../../util/filesystem.ts";
|
||||
import "../../util/zip.js";
|
||||
import { closeContextMenu } from "../fileManagerSlice.ts";
|
||||
import { DialogSelectOption, setBatchDownloadLog } from "../globalStateSlice.ts";
|
||||
import { AppThunk } from "../store.ts";
|
||||
import { promiseId, selectOption } from "./dialog.ts";
|
||||
import { longRunningTaskWithSnackbar, refreshSingleFileSymbolicLinks, walk, walkAll } from "./file.ts";
|
||||
|
||||
enum MultipleDownloadOption {
|
||||
Browser,
|
||||
StreamSaver,
|
||||
Backend,
|
||||
}
|
||||
|
||||
enum DownloadOverwriteOption {
|
||||
Skip,
|
||||
Overwrite,
|
||||
OverwriteAll,
|
||||
SkipAll,
|
||||
}
|
||||
|
||||
export function downloadFiles(index: number, files: FileResponse[]): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
dispatch(closeContextMenu({ index, value: undefined }));
|
||||
if (files.length == 1 && files[0].type == FileType.file) {
|
||||
await dispatch(downloadSingleFile(files[0]));
|
||||
} else {
|
||||
await dispatch(downloadMultipleFiles(files));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function downloadMultipleFiles(files: FileResponse[]): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
// Prepare download options
|
||||
const options: MultipleDownloadOption[] = [MultipleDownloadOption.StreamSaver];
|
||||
// @ts-ignore
|
||||
if (window.isSecureContext && window.showDirectoryPicker) {
|
||||
options.push(MultipleDownloadOption.Browser);
|
||||
}
|
||||
|
||||
const groupPermission = new Boolset(SessionManager.currentUser()?.group?.permission);
|
||||
if (
|
||||
groupPermission.enabled(GroupPermission.archive_download) &&
|
||||
(files.length > 1 || !files[0].metadata?.[Metadata.share_redirect])
|
||||
) {
|
||||
options.push(MultipleDownloadOption.Backend);
|
||||
}
|
||||
|
||||
let finalOption = options[0];
|
||||
if (options.length > 1) {
|
||||
try {
|
||||
finalOption = (await dispatch(
|
||||
selectOption(getDownloadSelectOption(options), "fileManager.selectArchiveMethod"),
|
||||
)) as MultipleDownloadOption;
|
||||
} catch (e) {
|
||||
// User cancel selection
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (finalOption == MultipleDownloadOption.Backend) {
|
||||
await dispatch(backendBatchDownload(files));
|
||||
} else if (finalOption == MultipleDownloadOption.Browser) {
|
||||
await dispatch(browserBatchDownload(files));
|
||||
} else {
|
||||
await dispatch(streamSaverDownload(files));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function backendBatchDownload(files: FileResponse[]): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
const downloadUrl = await longRunningTaskWithSnackbar(
|
||||
dispatch(
|
||||
getFileEntityUrl({
|
||||
uris: files.map((f) => getFileLinkedUri(f)),
|
||||
archive: true,
|
||||
}),
|
||||
),
|
||||
"application:fileManager.preparingBathDownload",
|
||||
);
|
||||
|
||||
window.location.assign(downloadUrl.urls[0].url);
|
||||
};
|
||||
}
|
||||
|
||||
export const cancelSignals: {
|
||||
[key: string]: AbortController;
|
||||
} = {};
|
||||
|
||||
export function browserBatchDownload(files: FileResponse[]): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
const downloadId = promiseId();
|
||||
cancelSignals[downloadId] = new AbortController();
|
||||
|
||||
// Select download folder
|
||||
let handle: FileSystemDirectoryHandle;
|
||||
if (!window.showDirectoryPicker || !window.isSecureContext) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// can't use suggestedName for showDirectoryPicker (only available showSaveFilePicker)
|
||||
handle = await window.showDirectoryPicker({
|
||||
startIn: "downloads",
|
||||
mode: "readwrite",
|
||||
});
|
||||
// we should obtain the readwrite permission for the directory at first
|
||||
if (!(await verifyFileSystemRWPermission(handle))) {
|
||||
enqueueSnackbar({
|
||||
message: i18next.t("application:fileManager.directoryDownloadPermissionError"),
|
||||
variant: "error",
|
||||
});
|
||||
throw new Error(i18next.t("application:fileManager.directoryDownloadPermissionError"));
|
||||
}
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
await longRunningTaskWithSnackbar(
|
||||
dispatch(startBrowserBatchDownloadTo(handle, downloadId, files)),
|
||||
"fileManager.batchDownloadStarted",
|
||||
ViewDownloadLogAction(downloadId),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function startBrowserBatchDownloadTo(
|
||||
handle: FileSystemDirectoryHandle,
|
||||
downloadId: string,
|
||||
files: FileResponse[],
|
||||
): AppThunk<Promise<void>> {
|
||||
return async (dispatch, _getState): Promise<void> => {
|
||||
let log = "";
|
||||
let failed = 0;
|
||||
let skipAll = false;
|
||||
let overwriteAll = false;
|
||||
|
||||
const appendLog = (msg: string) => {
|
||||
log = log + msg + "\n";
|
||||
dispatch(setBatchDownloadLog({ id: downloadId, logs: log }));
|
||||
};
|
||||
// get the files in the directory to compare with queue files
|
||||
// parent: ""
|
||||
const fsPaths = await getFileSystemDirectoryPaths(handle, "");
|
||||
|
||||
await dispatch(
|
||||
walk(files, async (children, relativePath) => {
|
||||
const childFiles = children.filter((f) => f.type == FileType.file);
|
||||
try {
|
||||
const entityUrls = await dispatch(
|
||||
getFileEntityUrl({
|
||||
uris: childFiles.map((f) => getFileLinkedUri(f)),
|
||||
download: true,
|
||||
skip_error: true,
|
||||
}),
|
||||
);
|
||||
|
||||
for (let i = 0; i < entityUrls.urls.length; i++) {
|
||||
if (!entityUrls.urls[i]) {
|
||||
appendLog(
|
||||
i18next.t("modals.directoryDownloadErrorNotification", {
|
||||
name: childFiles[i].name,
|
||||
msg: "failed to get download url",
|
||||
}),
|
||||
);
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
const name = (relativePath == "" ? "" : relativePath + "/") + childFiles[i].name;
|
||||
if (fsPaths.has(name)) {
|
||||
if (skipAll) {
|
||||
appendLog(
|
||||
i18next.t("modals.directoryDownloadSkipNotifiction", {
|
||||
name,
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (overwriteAll) {
|
||||
appendLog(
|
||||
i18next.t("modals.directoryDownloadReplaceNotifiction", {
|
||||
name,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
// No overwrite options, ask for one
|
||||
let overwriteOption = DownloadOverwriteOption.Skip;
|
||||
try {
|
||||
overwriteOption = (await dispatch(
|
||||
selectOption(getDownloadOverwriteOption(name), "fileManager.selectDirectoryDuplicationMethod"),
|
||||
)) as DownloadOverwriteOption;
|
||||
} catch (e) {
|
||||
// User cancel, use skip option
|
||||
overwriteOption = DownloadOverwriteOption.Skip;
|
||||
}
|
||||
|
||||
if (overwriteOption == DownloadOverwriteOption.Skip) {
|
||||
appendLog(
|
||||
i18next.t("modals.directoryDownloadSkipNotifiction", {
|
||||
name,
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
} else if (overwriteOption == DownloadOverwriteOption.SkipAll) {
|
||||
appendLog(
|
||||
i18next.t("modals.directoryDownloadSkipNotifiction", {
|
||||
name,
|
||||
}),
|
||||
);
|
||||
skipAll = true;
|
||||
continue;
|
||||
} else if (overwriteOption == DownloadOverwriteOption.OverwriteAll) {
|
||||
appendLog(
|
||||
i18next.t("modals.directoryDownloadReplaceNotifiction", {
|
||||
name,
|
||||
}),
|
||||
);
|
||||
overwriteAll = true;
|
||||
} else {
|
||||
appendLog(
|
||||
i18next.t("modals.directoryDownloadReplaceNotifiction", {
|
||||
name,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
appendLog(i18next.t("modals.directoryDownloadStarted", { name }));
|
||||
try {
|
||||
const res = await fetch(entityUrls.urls[i].url, {
|
||||
signal: cancelSignals[downloadId].signal,
|
||||
});
|
||||
await saveFileToFileSystemDirectory(handle, await res.blob(), name);
|
||||
appendLog(i18next.t("modals.directoryDownloadFinished", { name }));
|
||||
} catch (e) {
|
||||
// User cancel download
|
||||
if (e instanceof Error && e.name == "AbortError") {
|
||||
appendLog(i18next.t("modals.directoryDownloadCancelled"));
|
||||
throw e;
|
||||
}
|
||||
failed++;
|
||||
appendLog(
|
||||
i18next.t("modals.directoryDownloadErrorNotification", {
|
||||
name: name,
|
||||
msg: (e as Error).message,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.name == "AbortError") {
|
||||
throw e;
|
||||
}
|
||||
failed += childFiles.length;
|
||||
appendLog(
|
||||
i18next.t("modals.directoryDownloadError", {
|
||||
msg: (e as Error).message,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if (failed === 0) {
|
||||
appendLog(i18next.t("fileManager.directoryDownloadFinished"));
|
||||
} else {
|
||||
appendLog(i18next.t("fileManager.directoryDownloadFinishedWithError", { failed }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function streamSaverDownload(files: FileResponse[]): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
const allFiles = (
|
||||
await longRunningTaskWithSnackbar(dispatch(walkAll(files)), "application:fileManager.preparingBathDownload")
|
||||
).filter((f) => f.type == FileType.file);
|
||||
|
||||
const fileStream = streamSaver.createWriteStream(formatLocalTime(dayjs()) + ".zip");
|
||||
const {
|
||||
siteConfig: {
|
||||
explorer: {
|
||||
config: { max_batch_size },
|
||||
},
|
||||
},
|
||||
} = getState();
|
||||
const maxBatch = Math.min(10, max_batch_size ?? 1);
|
||||
let current = 0;
|
||||
|
||||
const readableZipStream = new (window as any).ZIP({
|
||||
start(_ctrl: any) {
|
||||
// ctrl.close()
|
||||
},
|
||||
async pull(ctrl: any) {
|
||||
const batch = allFiles.slice(current, current + maxBatch);
|
||||
current += batch.length;
|
||||
if (batch.length == 0) {
|
||||
ctrl.close();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const entityUrls = await dispatch(
|
||||
getFileEntityUrl({
|
||||
uris: batch.map((f) => getFileLinkedUri(f)),
|
||||
skip_error: true,
|
||||
download: true,
|
||||
}),
|
||||
);
|
||||
|
||||
for (let i = 0; i < entityUrls.urls.length; i++) {
|
||||
const url = entityUrls.urls[i];
|
||||
if (!url) {
|
||||
continue;
|
||||
}
|
||||
const res = await fetch(url.url);
|
||||
const stream = () => res.body;
|
||||
ctrl.enqueue({ name: batch[i].relativePath, stream });
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to get entity urls", e);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (window.WritableStream && readableZipStream.pipeTo) {
|
||||
try {
|
||||
await longRunningTaskWithSnackbar(readableZipStream.pipeTo(fileStream), "fileManager.batchDownloadStarted");
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function downloadSingleFile(file: FileResponse, preferredEntity?: string): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
const isSharedFile = file.metadata?.[Metadata.share_redirect] ?? false;
|
||||
if (isSharedFile) {
|
||||
file = await dispatch(refreshSingleFileSymbolicLinks(file));
|
||||
}
|
||||
|
||||
const urlRes = await longRunningTaskWithSnackbar(
|
||||
dispatch(
|
||||
getFileEntityUrl({
|
||||
uris: [getFileLinkedUri(file)],
|
||||
entity: preferredEntity,
|
||||
download: true,
|
||||
}),
|
||||
),
|
||||
"application:fileManager.preparingDownload",
|
||||
);
|
||||
|
||||
const streamSaverName = urlRes.urls[0].stream_saver_display_name;
|
||||
if (streamSaverName) {
|
||||
// remove streamSaverParam from query
|
||||
const fileStream = streamSaver.createWriteStream(streamSaverName);
|
||||
const res = await fetch(urlRes.urls[0].url);
|
||||
const readableStream = res.body;
|
||||
if (!readableStream) {
|
||||
return;
|
||||
}
|
||||
|
||||
// more optimized
|
||||
if (window.WritableStream && readableStream.pipeTo) {
|
||||
const downloadingSnackbar = enqueueSnackbar({
|
||||
message: i18next.t("fileManager.downloadingFile", {
|
||||
name: streamSaverName,
|
||||
}),
|
||||
variant: "loading",
|
||||
persist: true,
|
||||
});
|
||||
return readableStream.pipeTo(fileStream).finally(() => closeSnackbar(downloadingSnackbar));
|
||||
}
|
||||
} else {
|
||||
window.location.assign(urlRes.urls[0].url);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const getDownloadSelectOption = (options: MultipleDownloadOption[]): DialogSelectOption[] => {
|
||||
return options.map((option): DialogSelectOption => {
|
||||
switch (option) {
|
||||
case MultipleDownloadOption.Backend:
|
||||
return {
|
||||
value: MultipleDownloadOption.Backend,
|
||||
name: i18next.t("fileManager.serverBatchDownload"),
|
||||
description: i18next.t("fileManager.serverBatchDownloadDescription"),
|
||||
};
|
||||
case MultipleDownloadOption.Browser:
|
||||
return {
|
||||
value: MultipleDownloadOption.Browser,
|
||||
name: i18next.t("fileManager.browserDownload"),
|
||||
description: i18next.t("fileManager.browserDownloadDescription"),
|
||||
};
|
||||
default:
|
||||
return {
|
||||
value: MultipleDownloadOption.StreamSaver,
|
||||
name: i18next.t("fileManager.browserBatchDownload"),
|
||||
description: i18next.t("fileManager.browserBatchDownloadDescription"),
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getDownloadOverwriteOption = (name: string): DialogSelectOption[] => {
|
||||
return [
|
||||
{
|
||||
name: i18next.t("fileManager.directoryDownloadReplace"),
|
||||
description: i18next.t("fileManager.directoryDownloadReplaceDescription", { name }),
|
||||
value: DownloadOverwriteOption.Overwrite,
|
||||
},
|
||||
{
|
||||
name: i18next.t("fileManager.directoryDownloadSkip"),
|
||||
description: i18next.t("fileManager.directoryDownloadSkipDescription", {
|
||||
name,
|
||||
}),
|
||||
value: DownloadOverwriteOption.Skip,
|
||||
},
|
||||
{
|
||||
name: i18next.t("fileManager.directoryDownloadReplaceAll"),
|
||||
description: i18next.t("fileManager.directoryDownloadReplaceAllDescription", { name }),
|
||||
value: DownloadOverwriteOption.OverwriteAll,
|
||||
},
|
||||
{
|
||||
name: i18next.t("fileManager.directoryDownloadSkipAll"),
|
||||
description: i18next.t("fileManager.directoryDownloadSkipAllDescription", { name }),
|
||||
value: DownloadOverwriteOption.SkipAll,
|
||||
},
|
||||
];
|
||||
};
|
||||
1308
src/redux/thunks/file.ts
Executable file
1308
src/redux/thunks/file.ts
Executable file
File diff suppressed because it is too large
Load Diff
916
src/redux/thunks/filemanager.ts
Executable file
916
src/redux/thunks/filemanager.ts
Executable file
@@ -0,0 +1,916 @@
|
||||
import dayjs from "dayjs";
|
||||
import { getFileInfo, getFileList, getUserCapacity, sendPatchViewSync } from "../../api/api.ts";
|
||||
import { ExplorerView, FileResponse, FileType, ListResponse, Metadata } from "../../api/explorer.ts";
|
||||
import { getActionOpt } from "../../component/FileManager/ContextMenu/useActionDisplayOpt.ts";
|
||||
import { ListViewColumnSetting } from "../../component/FileManager/Explorer/ListView/Column.tsx";
|
||||
import { FileManagerIndex } from "../../component/FileManager/FileManager.tsx";
|
||||
import { getPaginationState } from "../../component/FileManager/Pagination/PaginationFooter.tsx";
|
||||
import { Condition, ConditionType } from "../../component/FileManager/Search/AdvanceSearch/ConditionBox.tsx";
|
||||
import { MinPageSize } from "../../component/FileManager/TopBar/ViewOptionPopover.tsx";
|
||||
import { SelectType } from "../../component/Uploader/core";
|
||||
import { Task } from "../../component/Uploader/core/types.ts";
|
||||
import { uploadPromisePool } from "../../component/Uploader/core/uploader/base.ts";
|
||||
import { defaultPath } from "../../hooks/useNavigation.tsx";
|
||||
import { router } from "../../router";
|
||||
import SessionManager, { UserSettings } from "../../session";
|
||||
import { getFileLinkedUri, sleep } from "../../util";
|
||||
import CrUri, { Filesystem, SearchParam, UriQuery } from "../../util/uri.ts";
|
||||
import {
|
||||
appendListResponse,
|
||||
appendTreeCache,
|
||||
applyListResponse,
|
||||
clearMultiSelectHovered,
|
||||
clearSelected,
|
||||
closeContextMenu,
|
||||
ContextMenuTypes,
|
||||
Layouts,
|
||||
resetFileManager,
|
||||
setCapacity,
|
||||
setContextMenu,
|
||||
setFmError,
|
||||
setFmLoading,
|
||||
setGalleryWidth,
|
||||
setLayout,
|
||||
setListViewColumns,
|
||||
setMultiSelectHovered,
|
||||
setPage,
|
||||
setPageSize,
|
||||
setPathProps,
|
||||
setSelected,
|
||||
setShowThumb,
|
||||
setSortOption,
|
||||
SingleManager,
|
||||
} from "../fileManagerSlice.ts";
|
||||
import {
|
||||
closeAdvanceSearch,
|
||||
closeImageViewer,
|
||||
resetDialogs,
|
||||
selectForUpload,
|
||||
setAdvanceSearch,
|
||||
setPinFileDialog,
|
||||
setSearchPopup,
|
||||
setShareReadmeDetect,
|
||||
setUploadFromClipboardDialog,
|
||||
setUploadRawFiles,
|
||||
} from "../globalStateSlice.ts";
|
||||
import { Viewers, ViewersByID } from "../siteConfigSlice.ts";
|
||||
import { AppThunk } from "../store.ts";
|
||||
import { promiseId } from "./dialog.ts";
|
||||
import { deleteFile, openFileContextMenu } from "./file.ts";
|
||||
import { queueLoadShareInfo } from "./share.ts";
|
||||
import { openViewer } from "./viewer.ts";
|
||||
|
||||
export function setTargetPath(index: number, path: string): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
try {
|
||||
const crUri = new CrUri(path);
|
||||
const pathElements = crUri.elements();
|
||||
const pure = crUri.pure_uri(UriQuery.category);
|
||||
dispatch(
|
||||
setPathProps({
|
||||
index,
|
||||
value: {
|
||||
path,
|
||||
path_elements: pathElements,
|
||||
path_root: crUri.base(),
|
||||
current_fs: crUri.fs(),
|
||||
pure_path_with_category: pure.toString().replace(/\/$/, ""),
|
||||
pure_path: crUri.pure_uri().toString(),
|
||||
path_root_with_category: pure.base(false),
|
||||
search_params: crUri.searchParams(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
dispatch(
|
||||
setFmError({
|
||||
index,
|
||||
value: e,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let generation = 0;
|
||||
export interface NavigateReconcileOptions {
|
||||
next_page?: boolean;
|
||||
sync_view?: boolean;
|
||||
}
|
||||
|
||||
const pageSize = (fm: SingleManager) => {
|
||||
let pageSize = Math.max(fm.pageSize, MinPageSize);
|
||||
return Math.min(fm.pageSize, fm.list?.props.max_page_size ?? pageSize);
|
||||
};
|
||||
|
||||
export function beforePathChange(index: number): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
const {
|
||||
globalState: { imageViewer },
|
||||
} = getState();
|
||||
if (imageViewer?.open && index == FileManagerIndex.main) {
|
||||
dispatch(closeImageViewer());
|
||||
}
|
||||
|
||||
dispatch(clearSelected({ index, value: undefined }));
|
||||
dispatch(clearMultiSelectHovered({ index, value: undefined }));
|
||||
};
|
||||
}
|
||||
|
||||
export function checkReadMeEnabled(index: number): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
const { path, current_fs } = getState().fileManager[index];
|
||||
if (path && current_fs == Filesystem.share) {
|
||||
try {
|
||||
const info = await dispatch(queueLoadShareInfo(new CrUri(path), false));
|
||||
dispatch(setShareReadmeDetect(info?.show_readme && info.source_type == FileType.folder));
|
||||
} catch (e) {
|
||||
dispatch(setShareReadmeDetect(false));
|
||||
}
|
||||
} else {
|
||||
dispatch(setShareReadmeDetect(false));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function checkOpenViewerQuery(index: number): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
const currentUrl = new URL(window.location.href);
|
||||
const viewer = currentUrl.searchParams.get("viewer");
|
||||
const fileId = currentUrl.searchParams.get("open");
|
||||
const version = currentUrl.searchParams.get("version");
|
||||
const size = currentUrl.searchParams.get("size");
|
||||
|
||||
// Clear viewer-related query parameters
|
||||
currentUrl.searchParams.delete("viewer");
|
||||
currentUrl.searchParams.delete("open");
|
||||
currentUrl.searchParams.delete("version");
|
||||
currentUrl.searchParams.delete("size");
|
||||
window.history.replaceState({}, "", currentUrl.toString());
|
||||
|
||||
if (!fileId || !viewer || !ViewersByID[viewer]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { files: list, pagination } = getState().fileManager[index]?.list ?? {};
|
||||
if (list) {
|
||||
// Find readme file from highest to lowest priority
|
||||
const found = list.find((file) => file.id === fileId);
|
||||
if (found) {
|
||||
dispatch(openViewer(found, ViewersByID[viewer], parseInt(size ?? "0"), version ?? undefined, true));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
alert("openViewer");
|
||||
};
|
||||
}
|
||||
|
||||
export function navigateReconcile(index: number, opt?: NavigateReconcileOptions): AppThunk<Promise<void>> {
|
||||
return async (dispatch, getState) => {
|
||||
const timeNow = dayjs().valueOf();
|
||||
const {
|
||||
fileManager,
|
||||
globalState: { sidebarOpen, imageViewer },
|
||||
} = getState();
|
||||
const { path, list, pure_path } = fileManager[index];
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentGeneration = ++generation;
|
||||
if (!opt?.next_page) {
|
||||
dispatch(setFmLoading({ index, value: true }));
|
||||
}
|
||||
dispatch(setFmError({ index, value: undefined }));
|
||||
|
||||
if (opt?.sync_view) {
|
||||
try {
|
||||
await dispatch(syncViewSettings(index));
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const currentLogin = SessionManager.currentLoginOrNull();
|
||||
const currentView = localCustomView[pure_path ?? ""];
|
||||
let useCustomView = currentLogin?.user.disable_view_sync || currentView;
|
||||
|
||||
let listRes: ListResponse | null = null;
|
||||
try {
|
||||
listRes = await dispatch(
|
||||
getFileList({
|
||||
next_page_token: opt && opt.next_page ? list?.pagination.next_token : undefined,
|
||||
uri: path,
|
||||
page: list?.pagination.page ?? undefined,
|
||||
...(useCustomView
|
||||
? {
|
||||
page_size: currentView?.page_size ?? pageSize(fileManager[index]),
|
||||
order_by: currentView?.order ?? fileManager[index].sortBy,
|
||||
order_direction: currentView?.order_direction ?? fileManager[index].sortDirection,
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
);
|
||||
|
||||
// DB sorting has limit on string comparison, so we need to
|
||||
// sort by localCompare, if all files in current page is loaded, and sortBy is name.
|
||||
const sortBy = listRes.view ? listRes.view.order : currentView?.order;
|
||||
const orderDirection = listRes.view ? listRes.view.order_direction : currentView?.order_direction;
|
||||
if (sortBy == "name" && !getPaginationState(list?.pagination).moreItems) {
|
||||
listRes.files = sortByLocalCompare(listRes.files, listRes.mixed_type, orderDirection == "desc");
|
||||
}
|
||||
} catch (e) {
|
||||
if (currentGeneration == generation) {
|
||||
dispatch(
|
||||
setFmError({
|
||||
index,
|
||||
value: e,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
if (currentGeneration == generation) {
|
||||
dispatch(setFmLoading({ index, value: false }));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if current request is stale
|
||||
if (currentGeneration !== generation) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (listRes) {
|
||||
const fsUri = new CrUri(path);
|
||||
dispatch(
|
||||
appendTreeCache({
|
||||
index,
|
||||
value: [listRes.files, fsUri.is_search() ? undefined : path],
|
||||
}),
|
||||
);
|
||||
|
||||
if (listRes.view) {
|
||||
// Apply view setting from cloud
|
||||
dispatch(setPageSize({ index, value: listRes.view.page_size }));
|
||||
dispatch(
|
||||
setSortOption({ index, value: [listRes.view.order ?? "created_at", listRes.view.order_direction ?? "asc"] }),
|
||||
);
|
||||
if (!currentView) {
|
||||
dispatch(setShowThumb({ index, value: !!listRes.view.thumbnail }));
|
||||
dispatch(setLayout({ index, value: listRes.view.view ?? Layouts.grid }));
|
||||
dispatch(
|
||||
setListViewColumns(listRes.view.columns ?? SessionManager.getWithFallback(UserSettings.ListViewColumns)),
|
||||
);
|
||||
dispatch(
|
||||
setGalleryWidth({
|
||||
index,
|
||||
value: listRes.view.gallery_width ?? SessionManager.getWithFallback(UserSettings.GalleryWidth),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentView) {
|
||||
// Apply view setting from local cache
|
||||
dispatch(setShowThumb({ index, value: !!currentView.thumbnail }));
|
||||
dispatch(setLayout({ index, value: currentView.view ?? Layouts.grid }));
|
||||
dispatch(
|
||||
setListViewColumns(currentView.columns ?? SessionManager.getWithFallback(UserSettings.ListViewColumns)),
|
||||
);
|
||||
dispatch(
|
||||
setGalleryWidth({
|
||||
index,
|
||||
value: currentView.gallery_width ?? SessionManager.getWithFallback(UserSettings.GalleryWidth),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (opt && opt.next_page) {
|
||||
dispatch(appendListResponse({ index, value: listRes }));
|
||||
} else {
|
||||
if (listRes.pagination.total_items) {
|
||||
// check if page is overflow
|
||||
const totalPages = Math.ceil(listRes.pagination.total_items / listRes.pagination.page_size) - 1;
|
||||
if (listRes.pagination.page > totalPages) {
|
||||
dispatch(changePage(index, totalPages));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fill in minimum 150ms for transition animation
|
||||
const timeDiff = dayjs().valueOf() - timeNow;
|
||||
if (timeDiff > 0 && timeDiff < 140) {
|
||||
await sleep(140 - timeDiff);
|
||||
}
|
||||
dispatch(applyListResponse({ index, value: listRes }));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function loadMorePages(index: number): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
const fm = getState().fileManager[index];
|
||||
if (fm.list?.pagination.next_token) {
|
||||
dispatch(navigateReconcile(index, { next_page: true }));
|
||||
} else {
|
||||
dispatch(changePage(index, (fm.list?.pagination.page ?? 0) + 1));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function loadChild(index: number, path: string, beforeLoad?: () => void): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
let listRes: ListResponse | null = null;
|
||||
const { fileManager } = getState();
|
||||
const current = fileManager[index].tree[path];
|
||||
if (current && current.children) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (beforeLoad) {
|
||||
beforeLoad();
|
||||
}
|
||||
listRes = await dispatch(
|
||||
getFileList({
|
||||
page_size: pageSize(fileManager[index]),
|
||||
uri: path,
|
||||
order_by: fileManager[index].sortBy,
|
||||
order_direction: fileManager[index].sortDirection,
|
||||
}),
|
||||
);
|
||||
dispatch(appendTreeCache({ index, value: [listRes.files, path] }));
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function changePageSize(index: number, pageSize: number): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
SessionManager.set(UserSettings.PageSize, pageSize);
|
||||
dispatch(setPageSize({ index, value: pageSize }));
|
||||
dispatch(navigateReconcile(index, { sync_view: true }));
|
||||
};
|
||||
}
|
||||
|
||||
export function changePage(index: number, page: number): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
dispatch(setPage({ index, value: page }));
|
||||
dispatch(navigateReconcile(index));
|
||||
};
|
||||
}
|
||||
|
||||
export function changeSortOption(index: number, sortBy: string, sortDirection: string): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
dispatch(setSortOption({ index, value: [sortBy, sortDirection] }));
|
||||
SessionManager.set(UserSettings.SortBy, sortBy);
|
||||
SessionManager.set(UserSettings.SortDirection, sortDirection);
|
||||
dispatch(navigateReconcile(index, { sync_view: true }));
|
||||
};
|
||||
}
|
||||
|
||||
export function updateUserCapacity(index: number): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
if (!SessionManager.currentLoginOrNull()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const capacity = await dispatch(getUserCapacity());
|
||||
dispatch(setCapacity({ index, value: capacity }));
|
||||
} catch (e) {
|
||||
console.warn("Failed to load user capacity", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function fileHovered(index: number, file: FileResponse, hovered: boolean): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
const fileManager = getState().fileManager[index];
|
||||
const hasFileSelected = Object.keys(fileManager.selected).length > 0;
|
||||
if (!hasFileSelected && hovered) {
|
||||
return;
|
||||
}
|
||||
dispatch(setMultiSelectHovered({ index, value: [file.path, hovered] }));
|
||||
};
|
||||
}
|
||||
|
||||
export function refreshFileList(index: number): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
dispatch(closeContextMenu({ index, value: undefined }));
|
||||
await dispatch(navigateReconcile(index));
|
||||
dispatch(clearSelected({ index, value: undefined }));
|
||||
};
|
||||
}
|
||||
|
||||
export function openEmptyContextMenu(index: number, e: React.MouseEvent<HTMLElement>): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
e.preventDefault();
|
||||
const { x, y } = { x: e.clientX, y: e.clientY };
|
||||
dispatch(clearSelected({ index, value: undefined }));
|
||||
dispatch(
|
||||
setContextMenu({
|
||||
index,
|
||||
value: {
|
||||
open: true,
|
||||
pos: { x, y },
|
||||
type: ContextMenuTypes.empty,
|
||||
fmIndex: index,
|
||||
targets: undefined,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function openNewContextMenu(index: number, e: React.MouseEvent<HTMLElement>): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
e.preventDefault();
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const { x, y } = { x: rect.x, y: rect.bottom };
|
||||
dispatch(
|
||||
setContextMenu({
|
||||
index,
|
||||
value: {
|
||||
open: true,
|
||||
pos: { x, y },
|
||||
type: ContextMenuTypes.new,
|
||||
fmIndex: index,
|
||||
targets: undefined,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function pinCurrentView(index: number): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
const path = getState().fileManager[index].path;
|
||||
dispatch(setPinFileDialog({ open: true, uri: path }));
|
||||
};
|
||||
}
|
||||
|
||||
export function navigateToPath(
|
||||
index: number,
|
||||
path: string,
|
||||
file: FileResponse | undefined = undefined,
|
||||
newTab: boolean = false,
|
||||
): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
if (file) {
|
||||
path = getFileLinkedUri(file);
|
||||
}
|
||||
|
||||
try {
|
||||
const crUri = new CrUri(path);
|
||||
const currentUser = SessionManager.currentLoginOrNull();
|
||||
if (crUri.id() && crUri.fs() == Filesystem.my && currentUser && currentUser.user.id == crUri.id()) {
|
||||
crUri.setUsername("");
|
||||
path = crUri.toString();
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
if (index == FileManagerIndex.selector) {
|
||||
return dispatch(setTargetPath(index, path));
|
||||
}
|
||||
|
||||
const s = new URLSearchParams("?path=" + encodeURIComponent(path));
|
||||
const uri = "/home?" + s.toString();
|
||||
if (newTab) {
|
||||
// Open in new tab
|
||||
window.open(uri, "_blank");
|
||||
return;
|
||||
}
|
||||
router.navigate(uri);
|
||||
};
|
||||
}
|
||||
|
||||
export function retrySharePassword(index: number, password: string): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
const { fileManager } = getState();
|
||||
const fm = fileManager[index];
|
||||
if (!fm.path) {
|
||||
return;
|
||||
}
|
||||
const crUri = new CrUri(fm.path);
|
||||
crUri.setPassword(password);
|
||||
|
||||
console.log(crUri.toString());
|
||||
dispatch(navigateToPath(index, crUri.toString()));
|
||||
};
|
||||
}
|
||||
|
||||
export function searchMetadata(
|
||||
index: number,
|
||||
metaKey: string,
|
||||
metaValue?: string,
|
||||
newTab?: boolean,
|
||||
strongMatch?: boolean,
|
||||
): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
const { fileManager } = getState();
|
||||
const fm = fileManager[index];
|
||||
const root = fm.path_root;
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rootUri = new CrUri(root);
|
||||
rootUri.addQuery(
|
||||
(strongMatch ? UriQuery.metadata_strong_match : UriQuery.metadata_prefix) + metaKey,
|
||||
metaValue ?? "",
|
||||
);
|
||||
dispatch(navigateToPath(index, rootUri.toString(), undefined, newTab));
|
||||
};
|
||||
}
|
||||
|
||||
export function uploadClicked(index: number, type: SelectType): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
dispatch(closeContextMenu({ index, value: undefined }));
|
||||
dispatch(selectForUpload({ type }));
|
||||
};
|
||||
}
|
||||
|
||||
export function uploadFromClipboard(index: number): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
dispatch(closeContextMenu({ index, value: undefined }));
|
||||
dispatch(setUploadFromClipboardDialog(true));
|
||||
};
|
||||
}
|
||||
|
||||
export function resetFm(index: number): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
dispatch(resetFileManager(index));
|
||||
dispatch(resetDialogs());
|
||||
};
|
||||
}
|
||||
|
||||
// Regex to split by spaces, but keep anything in quotes together
|
||||
const SPACE_RE = / +(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/;
|
||||
|
||||
export function quickSearch(index: number, base: string, keyword: string): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
const crUri = new CrUri(base);
|
||||
crUri.setSearchParam({
|
||||
name: keyword.split(SPACE_RE).filter((x) => x),
|
||||
case_folding: true,
|
||||
});
|
||||
dispatch(navigateToPath(index, crUri.toString()));
|
||||
dispatch(setSearchPopup(false));
|
||||
};
|
||||
}
|
||||
|
||||
export function advancedSearch(index: number, conditions: Condition[]): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
const params: SearchParam = {};
|
||||
let base = "";
|
||||
conditions.forEach((condition) => {
|
||||
switch (condition.type) {
|
||||
case ConditionType.base:
|
||||
base = condition.base_uri ?? defaultPath;
|
||||
break;
|
||||
case ConditionType.name:
|
||||
if (condition.names?.length ?? 0 > 0) {
|
||||
params.name = condition.names;
|
||||
params.name_op_or = condition.name_op_or;
|
||||
params.case_folding = condition.case_folding;
|
||||
}
|
||||
break;
|
||||
case ConditionType.type:
|
||||
params.type = condition.file_type;
|
||||
break;
|
||||
case ConditionType.tag:
|
||||
if (!params.metadata) {
|
||||
params.metadata = {};
|
||||
}
|
||||
if (condition.tags) {
|
||||
condition.tags.forEach((tag) => {
|
||||
if (params.metadata) {
|
||||
params.metadata[Metadata.tag_prefix + tag] = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case ConditionType.metadata:
|
||||
if (condition.metadata_key && !condition.metadata_strong_match) {
|
||||
if (!params.metadata) {
|
||||
params.metadata = {};
|
||||
}
|
||||
params.metadata[condition.metadata_key] = condition.metadata_value ?? "";
|
||||
}
|
||||
if (condition.metadata_key && condition.metadata_strong_match) {
|
||||
if (!params.metadata_strong_match) {
|
||||
params.metadata_strong_match = {};
|
||||
}
|
||||
params.metadata_strong_match[condition.metadata_key] = condition.metadata_value ?? "";
|
||||
}
|
||||
break;
|
||||
case ConditionType.size:
|
||||
if (condition.size_gte != undefined || condition.size_lte != undefined) {
|
||||
params.size_gte = condition.size_gte;
|
||||
params.size_lte = condition.size_lte;
|
||||
}
|
||||
break;
|
||||
case ConditionType.created:
|
||||
if (condition.created_gte != undefined || condition.created_lte != undefined) {
|
||||
params.created_at_gte = condition.created_gte;
|
||||
params.created_at_lte = condition.created_lte;
|
||||
}
|
||||
break;
|
||||
case ConditionType.modified:
|
||||
if (condition.updated_gte != undefined || condition.updated_lte != undefined) {
|
||||
params.updated_at_gte = condition.updated_gte;
|
||||
params.updated_at_lte = condition.updated_lte;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const crUri = new CrUri(base);
|
||||
crUri.setSearchParam(params);
|
||||
dispatch(navigateToPath(index, crUri.toString()));
|
||||
dispatch(closeAdvanceSearch());
|
||||
};
|
||||
}
|
||||
|
||||
export function openAdvancedSearch(index: number, initialKeywords?: string): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
const current_base = getState().fileManager[index].pure_path;
|
||||
dispatch(setSearchPopup(false));
|
||||
dispatch(
|
||||
setAdvanceSearch({
|
||||
open: true,
|
||||
basePath: current_base ?? defaultPath,
|
||||
nameCondition: initialKeywords != undefined ? initialKeywords.split(SPACE_RE).filter((x) => x) : undefined,
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function clearSearch(index: number): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch(navigateToPath(index, getState().fileManager[index]?.pure_path ?? defaultPath));
|
||||
};
|
||||
}
|
||||
|
||||
export function selectAll(index: number): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
const fm = getState().fileManager[index];
|
||||
const files = fm.list?.files;
|
||||
if (!files) {
|
||||
return;
|
||||
}
|
||||
dispatch(setSelected({ index, value: files }));
|
||||
};
|
||||
}
|
||||
|
||||
export function shortCutDelete(index: number): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
const selected = Object.values(getState().fileManager[index].selected);
|
||||
const actionOpt = getActionOpt(selected, Viewers);
|
||||
if (actionOpt.showDelete) {
|
||||
dispatch(deleteFile(index, selected));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function inverseSelection(index: number): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
const fm = getState().fileManager[index];
|
||||
const files = fm.list?.files;
|
||||
if (!files) {
|
||||
return;
|
||||
}
|
||||
const selected = Object.values(fm.selected);
|
||||
const newSelected = files.filter((file) => !selected.includes(file));
|
||||
dispatch(setSelected({ index, value: newSelected }));
|
||||
};
|
||||
}
|
||||
|
||||
export function openContextUrlFromUri(index: number, uri: string, e: React.MouseEvent<HTMLElement>): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
// try get file from tree cache
|
||||
let file = getState().fileManager[index].tree[uri]?.file;
|
||||
if (!file) {
|
||||
try {
|
||||
file = await dispatch(getFileInfo({ uri }));
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(openFileContextMenu(index, file, true, e, ContextMenuTypes.file, false));
|
||||
};
|
||||
}
|
||||
|
||||
export function setThumbToggle(index: number, value: boolean): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
dispatch(setFmLoading({ index, value: true }));
|
||||
await dispatch(syncViewSettings(index, undefined, undefined, value));
|
||||
dispatch(setShowThumb({ index: index, value: value }));
|
||||
SessionManager.set(UserSettings.ShowThumb, value);
|
||||
dispatch(setFmLoading({ index, value: false }));
|
||||
};
|
||||
}
|
||||
|
||||
export function setLayoutSetting(index: number, value: string): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
dispatch(setFmLoading({ index, value: true }));
|
||||
dispatch(setLayout({ index: index, value: value }));
|
||||
SessionManager.set(UserSettings.Layout, value);
|
||||
await dispatch(syncViewSettings(index));
|
||||
dispatch(setFmLoading({ index, value: false }));
|
||||
};
|
||||
}
|
||||
|
||||
let localCustomView: Record<string, ExplorerView> = {};
|
||||
|
||||
export const clearLocalCustomView = () => {
|
||||
localCustomView = {};
|
||||
};
|
||||
|
||||
export function syncViewSettings(
|
||||
index: number,
|
||||
columns?: ListViewColumnSetting[],
|
||||
galleryWidth?: number,
|
||||
thumbOff?: boolean,
|
||||
): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
const fm = getState().fileManager[index];
|
||||
const currentLogin = SessionManager.currentLoginOrNull();
|
||||
if (!fm.list || !fm.pure_path) {
|
||||
return;
|
||||
}
|
||||
const parent = fm.list.parent;
|
||||
const crUri = new CrUri(fm.pure_path);
|
||||
const shouldUpdatedView =
|
||||
currentLogin &&
|
||||
!currentLogin.user.disable_view_sync &&
|
||||
(parent?.owned || crUri.fs() == Filesystem.trash || crUri.fs() == Filesystem.shared_with_me);
|
||||
|
||||
const currentView: ExplorerView = {
|
||||
page_size: Math.max(MinPageSize, pageSize(fm)),
|
||||
order: fm.sortBy ?? "created_at",
|
||||
order_direction: fm.sortDirection ?? "asc",
|
||||
view: fm.layout ?? Layouts.grid,
|
||||
thumbnail: thumbOff ?? fm.showThumb,
|
||||
columns: columns ?? fm.listViewColumns,
|
||||
gallery_width: galleryWidth ?? fm.galleryWidth ?? 110,
|
||||
};
|
||||
|
||||
if (shouldUpdatedView) {
|
||||
await dispatch(
|
||||
sendPatchViewSync({
|
||||
uri: fm.pure_path,
|
||||
view: currentView,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
localCustomView[fm.pure_path] = currentView;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function applyListColumns(index: number, columns: ListViewColumnSetting[]): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
dispatch(setListViewColumns(columns));
|
||||
SessionManager.set(UserSettings.ListViewColumns, columns);
|
||||
dispatch(syncViewSettings(index, columns));
|
||||
};
|
||||
}
|
||||
|
||||
export function applyGalleryWidth(index: number, width: number): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
dispatch(setFmLoading({ index, value: true }));
|
||||
await dispatch(syncViewSettings(index, undefined, width));
|
||||
dispatch(setGalleryWidth({ index, value: width }));
|
||||
SessionManager.set(UserSettings.GalleryWidth, width);
|
||||
dispatch(setFmLoading({ index, value: false }));
|
||||
};
|
||||
}
|
||||
|
||||
export function uploadRawFile(files: File): AppThunk<Promise<Task>> {
|
||||
return async (dispatch, _getState) => {
|
||||
const id = promiseId();
|
||||
return new Promise<Task>((resolve, reject) => {
|
||||
uploadPromisePool[id] = { resolve, reject };
|
||||
dispatch(
|
||||
setUploadRawFiles({
|
||||
files: [files],
|
||||
promiseId: [id],
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function sortByLocalCompare(files: FileResponse[], mixed?: boolean, isDesc?: boolean): FileResponse[] {
|
||||
if (files.length === 0) {
|
||||
return files;
|
||||
}
|
||||
|
||||
const descending = isDesc ?? false;
|
||||
|
||||
// If mixed is true, sort all files together
|
||||
if (mixed) {
|
||||
return files.slice().sort((a, b) => {
|
||||
const result = a.name.localeCompare(b.name);
|
||||
return descending ? -result : result;
|
||||
});
|
||||
}
|
||||
|
||||
// If mixed is false, separate folders and files, then sort each part
|
||||
const sortedFiles = files.slice();
|
||||
|
||||
// Binary search to find the division between folders and files
|
||||
let left = 0;
|
||||
let right = sortedFiles.length - 1;
|
||||
let divisionIndex = -1;
|
||||
|
||||
// First, we need to find if there's a division at all
|
||||
let hasFolder = false;
|
||||
let hasFile = false;
|
||||
for (const file of sortedFiles) {
|
||||
if (file.type === FileType.folder) hasFolder = true;
|
||||
if (file.type === FileType.file) hasFile = true;
|
||||
}
|
||||
|
||||
if (!hasFolder || !hasFile) {
|
||||
// All items are the same type, just sort normally
|
||||
return sortedFiles.sort((a, b) => {
|
||||
const result = a.name.localeCompare(b.name, navigator.languages[0] || navigator.language, {
|
||||
numeric: true,
|
||||
ignorePunctuation: true,
|
||||
});
|
||||
return descending ? -result : result;
|
||||
});
|
||||
}
|
||||
|
||||
// Find the division using binary search
|
||||
// We're looking for the first file (type 0) after folders (type 1)
|
||||
while (left <= right) {
|
||||
const mid = Math.floor((left + right) / 2);
|
||||
|
||||
if (sortedFiles[mid].type === FileType.folder) {
|
||||
// Check if next item is a file
|
||||
if (mid + 1 < sortedFiles.length && sortedFiles[mid + 1].type === FileType.file) {
|
||||
divisionIndex = mid + 1;
|
||||
break;
|
||||
}
|
||||
left = mid + 1;
|
||||
} else {
|
||||
// This is a file, look left for the division
|
||||
if (mid === 0 || sortedFiles[mid - 1].type === FileType.folder) {
|
||||
divisionIndex = mid;
|
||||
break;
|
||||
}
|
||||
right = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If no clear division found, fallback to linear search
|
||||
if (divisionIndex === -1) {
|
||||
for (let i = 0; i < sortedFiles.length; i++) {
|
||||
if (sortedFiles[i].type === FileType.file) {
|
||||
divisionIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let folders: FileResponse[] = [];
|
||||
let filesOnly: FileResponse[] = [];
|
||||
|
||||
if (divisionIndex === -1) {
|
||||
// All are folders
|
||||
folders = sortedFiles;
|
||||
} else if (divisionIndex === 0) {
|
||||
// All are files
|
||||
filesOnly = sortedFiles;
|
||||
} else {
|
||||
// Split into folders and files
|
||||
folders = sortedFiles.slice(0, divisionIndex);
|
||||
filesOnly = sortedFiles.slice(divisionIndex);
|
||||
}
|
||||
|
||||
// Sort folders by name
|
||||
folders.sort((a, b) => {
|
||||
const result = a.name.localeCompare(b.name, navigator.languages[0] || navigator.language, {
|
||||
numeric: true,
|
||||
ignorePunctuation: true,
|
||||
});
|
||||
return descending ? -result : result;
|
||||
});
|
||||
|
||||
// Sort files by name
|
||||
filesOnly.sort((a, b) => {
|
||||
const result = a.name.localeCompare(b.name, navigator.languages[0] || navigator.language, {
|
||||
numeric: true,
|
||||
ignorePunctuation: true,
|
||||
});
|
||||
return descending ? -result : result;
|
||||
});
|
||||
|
||||
// Return folders first, then files
|
||||
return [...folders, ...filesOnly];
|
||||
}
|
||||
78
src/redux/thunks/session.ts
Executable file
78
src/redux/thunks/session.ts
Executable file
@@ -0,0 +1,78 @@
|
||||
import i18next from "i18next";
|
||||
import { getUserInfo, sendSignout } from "../../api/api.ts";
|
||||
import { LoginResponse, User } from "../../api/user.ts";
|
||||
import { router } from "../../router";
|
||||
import SessionManager, { UserSettings } from "../../session";
|
||||
import { refreshTimeZone } from "../../util/datetime.ts";
|
||||
import { clearSessionCache } from "../fileManagerSlice.ts";
|
||||
import {
|
||||
closeMusicPlayer,
|
||||
setDarkMode,
|
||||
setDrawerWidth,
|
||||
setPolicyOptionCache,
|
||||
setPreferredTheme,
|
||||
setUserInfoCache,
|
||||
} from "../globalStateSlice.ts";
|
||||
import { AppThunk } from "../store.ts";
|
||||
import { longRunningTaskWithSnackbar } from "./file.ts";
|
||||
import { updateSiteConfig } from "./site.ts";
|
||||
|
||||
export function refreshUserSession(session: LoginResponse, redirect: string | null): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
dispatch(setTargetSession(session));
|
||||
dispatch(updateSiteConfig());
|
||||
|
||||
if (redirect) {
|
||||
router.navigate(redirect);
|
||||
} else {
|
||||
router.navigate("/home");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function setTargetSession(session: LoginResponse): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
SessionManager.upsert(session);
|
||||
dispatch(setPreferredTheme(session.user.preferred_theme ?? ""));
|
||||
if (session.user.language) {
|
||||
i18next.changeLanguage(session.user.language);
|
||||
}
|
||||
dispatch(setDrawerWidth(SessionManager.getWithFallback(UserSettings.DrawerWidth)));
|
||||
dispatch(setDarkMode(SessionManager.get(UserSettings.PreferredDarkMode)));
|
||||
// TODO: clear fm cache
|
||||
dispatch(setPolicyOptionCache());
|
||||
dispatch(clearSessionCache({ index: 0, value: undefined }));
|
||||
refreshTimeZone();
|
||||
};
|
||||
}
|
||||
|
||||
export function loadUserInfo(uid: string): AppThunk<Promise<User>> {
|
||||
return async (dispatch, getState) => {
|
||||
const userInfoCache = getState().globalState.userInfoCache;
|
||||
if (userInfoCache[uid]) {
|
||||
return userInfoCache[uid];
|
||||
}
|
||||
|
||||
const user = await dispatch(getUserInfo(uid));
|
||||
dispatch(setUserInfoCache([uid, user]));
|
||||
return user;
|
||||
};
|
||||
}
|
||||
|
||||
export function signout(): AppThunk<void> {
|
||||
return async (dispatch, _getState) => {
|
||||
const current = SessionManager.currentLoginOrNull();
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
|
||||
await longRunningTaskWithSnackbar(
|
||||
dispatch(sendSignout({ refresh_token: current.token.refresh_token })),
|
||||
"application:login.signingOut",
|
||||
);
|
||||
|
||||
router.navigate("/session");
|
||||
dispatch(closeMusicPlayer());
|
||||
SessionManager.signOutCurrent();
|
||||
};
|
||||
}
|
||||
41
src/redux/thunks/settings.ts
Executable file
41
src/redux/thunks/settings.ts
Executable file
@@ -0,0 +1,41 @@
|
||||
import { AppThunk } from "../store.ts";
|
||||
import { sendPinFile, sendUnpinFile, sendUpdateUserSetting } from "../../api/api.ts";
|
||||
import { updateSiteConfig } from "./site.ts";
|
||||
import { increasePinedGeneration } from "../globalStateSlice.ts";
|
||||
import i18next from "i18next";
|
||||
|
||||
export function pinToSidebar(uri: string, name?: string): AppThunk<Promise<void>> {
|
||||
return async (dispatch, _getState) => {
|
||||
await dispatch(
|
||||
sendPinFile({
|
||||
uri,
|
||||
name,
|
||||
}),
|
||||
);
|
||||
await dispatch(updateSiteConfig());
|
||||
dispatch(increasePinedGeneration());
|
||||
};
|
||||
}
|
||||
|
||||
export function unPinFromSidebar(uri: string): AppThunk<Promise<void>> {
|
||||
return async (dispatch, _getState) => {
|
||||
await dispatch(
|
||||
sendUnpinFile({
|
||||
uri,
|
||||
}),
|
||||
);
|
||||
await dispatch(updateSiteConfig());
|
||||
dispatch(increasePinedGeneration());
|
||||
};
|
||||
}
|
||||
|
||||
export function selectLanguage(lng: string): AppThunk<Promise<void>> {
|
||||
return async (dispatch, _getState) => {
|
||||
await i18next.changeLanguage(lng);
|
||||
await dispatch(
|
||||
sendUpdateUserSetting({
|
||||
language: lng,
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
227
src/redux/thunks/share.ts
Executable file
227
src/redux/thunks/share.ts
Executable file
@@ -0,0 +1,227 @@
|
||||
import i18next from "i18next";
|
||||
import { closeSnackbar, enqueueSnackbar, SnackbarKey } from "notistack";
|
||||
import { getFileInfo, getFileList, getShareInfo, sendCreateShare, sendUpdateShare } from "../../api/api.ts";
|
||||
import { FileResponse, Share, ShareCreateService } from "../../api/explorer.ts";
|
||||
import { DefaultCloseAction, OpenReadMeAction } from "../../component/Common/Snackbar/snackbar.tsx";
|
||||
import { ShareSetting } from "../../component/FileManager/Dialogs/Share/ShareSetting.tsx";
|
||||
import { getPaginationState } from "../../component/FileManager/Pagination/PaginationFooter.tsx";
|
||||
import CrUri from "../../util/uri.ts";
|
||||
import { fileUpdated } from "../fileManagerSlice.ts";
|
||||
import {
|
||||
addShareInfo,
|
||||
closeShareReadme,
|
||||
setManageShareDialog,
|
||||
setShareLinkDialog,
|
||||
setShareReadmeOpen,
|
||||
} from "../globalStateSlice.ts";
|
||||
import { AppThunk } from "../store.ts";
|
||||
import { longRunningTaskWithSnackbar } from "./file.ts";
|
||||
|
||||
export function createOrUpdateShareLink(
|
||||
index: number,
|
||||
file: FileResponse,
|
||||
setting: ShareSetting,
|
||||
existed?: string,
|
||||
): AppThunk<Promise<string>> {
|
||||
return async (dispatch, getState) => {
|
||||
const req: ShareCreateService = {
|
||||
uri: file.path,
|
||||
is_private: setting.is_private,
|
||||
password: setting.password,
|
||||
share_view: setting.share_view,
|
||||
show_readme: setting.show_readme,
|
||||
downloads: setting.downloads && setting.downloads_val.value > 0 ? setting.downloads_val.value : undefined,
|
||||
expire: setting.expires && setting.expires_val.value > 0 ? setting.expires_val.value : undefined,
|
||||
};
|
||||
|
||||
const res = await dispatch(existed ? sendUpdateShare(req, existed) : sendCreateShare(req));
|
||||
dispatch(
|
||||
fileUpdated({
|
||||
index,
|
||||
value: [
|
||||
{
|
||||
file: { ...file, shared: true },
|
||||
oldPath: file.path,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
if (existed) {
|
||||
const {
|
||||
globalState: { manageShareDialogOpen, manageShareDialogFile },
|
||||
} = getState();
|
||||
if (manageShareDialogOpen && manageShareDialogFile?.path === file.path) {
|
||||
dispatch(
|
||||
setManageShareDialog({
|
||||
open: true,
|
||||
file: {
|
||||
...manageShareDialogFile,
|
||||
extended_info: undefined,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
}
|
||||
|
||||
interface shareInfoQueueItem {
|
||||
resolve: (value: Share | PromiseLike<Share>) => void;
|
||||
reject: (reason?: any) => void;
|
||||
}
|
||||
|
||||
const shareInfoLoadQueue: {
|
||||
[key: string]: shareInfoQueueItem[];
|
||||
} = {};
|
||||
|
||||
export function queueLoadShareInfo(uri: CrUri, countViews: boolean = false): AppThunk<Promise<Share>> {
|
||||
return async (dispatch, getState) => {
|
||||
const id = `${uri.id()}/${uri.password()}/${countViews}`;
|
||||
const cached = getState().globalState.shareInfo[id];
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
if (!shareInfoLoadQueue[id]) {
|
||||
shareInfoLoadQueue[id] = [];
|
||||
}
|
||||
|
||||
const p = new Promise<Share>((resolve, reject) => {
|
||||
shareInfoLoadQueue[id].push({ resolve, reject });
|
||||
});
|
||||
|
||||
if (shareInfoLoadQueue[id].length === 1) {
|
||||
dispatch(getShareInfo(uri.id(), uri.password(), countViews))
|
||||
.then((res) => {
|
||||
shareInfoLoadQueue[id].forEach((item) => {
|
||||
item.resolve(res);
|
||||
});
|
||||
dispatch(addShareInfo({ id, info: res }));
|
||||
})
|
||||
.catch((e) => {
|
||||
shareInfoLoadQueue[id].forEach((item) => {
|
||||
item.reject(e);
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
delete shareInfoLoadQueue[id];
|
||||
});
|
||||
}
|
||||
|
||||
return p;
|
||||
};
|
||||
}
|
||||
|
||||
export function openShareEditByID(shareId: string, password?: string, singleFile?: boolean): AppThunk {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
const { share, file } = await longRunningTaskWithSnackbar(
|
||||
dispatch(getFileAndShareById(shareId, password, singleFile)),
|
||||
"application:uploader.processing",
|
||||
);
|
||||
dispatch(
|
||||
setShareLinkDialog({
|
||||
open: true,
|
||||
file: file,
|
||||
share: share,
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Priority from high to low
|
||||
const supportedReadMeFiles = ["README.md", "README.txt"];
|
||||
|
||||
export function detectReadMe(index: number, isTablet: boolean): AppThunk<Promise<void>> {
|
||||
return async (dispatch, getState) => {
|
||||
const { files: list, pagination } = getState().fileManager[index]?.list ?? {};
|
||||
if (list) {
|
||||
// Find readme file from highest to lowest priority
|
||||
for (const readmeFile of supportedReadMeFiles) {
|
||||
const found = list.find((file) => file.name === readmeFile);
|
||||
if (found) {
|
||||
dispatch(tryOpenReadMe(found, isTablet));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not found in current file list, try to get file directly
|
||||
const path = getState().fileManager[index]?.pure_path;
|
||||
const hasMorePages = getPaginationState(pagination).moreItems;
|
||||
if (path && hasMorePages) {
|
||||
const uri = new CrUri(path);
|
||||
for (const readmeFile of supportedReadMeFiles) {
|
||||
try {
|
||||
const file = await dispatch(getFileInfo({ uri: uri.join(readmeFile).toString() }, true));
|
||||
if (file) {
|
||||
dispatch(tryOpenReadMe(file, isTablet));
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
dispatch(closeShareReadme());
|
||||
};
|
||||
}
|
||||
|
||||
let snackbarId: SnackbarKey | undefined = undefined;
|
||||
|
||||
function tryOpenReadMe(file: FileResponse, askForConfirmation?: boolean): AppThunk<Promise<void>> {
|
||||
return async (dispatch) => {
|
||||
if (askForConfirmation) {
|
||||
dispatch(setShareReadmeOpen({ open: false, target: file }));
|
||||
if (snackbarId) {
|
||||
closeSnackbar(snackbarId);
|
||||
}
|
||||
snackbarId = enqueueSnackbar({
|
||||
message: "README.md",
|
||||
variant: "file",
|
||||
file,
|
||||
action: OpenReadMeAction(file),
|
||||
});
|
||||
} else {
|
||||
dispatch(setShareReadmeOpen({ open: true, target: file }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getFileAndShareById(
|
||||
shareId: string,
|
||||
password?: string,
|
||||
singleFile?: boolean,
|
||||
): AppThunk<
|
||||
Promise<{
|
||||
share: Share;
|
||||
file: FileResponse;
|
||||
}>
|
||||
> {
|
||||
return async (dispatch) => {
|
||||
let share: Share | undefined;
|
||||
try {
|
||||
share = await dispatch(getShareInfo(shareId, password, false, true));
|
||||
} catch (e) {
|
||||
enqueueSnackbar({
|
||||
message: i18next.t("application:share.shareNotExist"),
|
||||
variant: "error",
|
||||
action: DefaultCloseAction,
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
|
||||
let file: FileResponse | undefined = undefined;
|
||||
if (singleFile) {
|
||||
const root = new CrUri(share.source_uri ?? "");
|
||||
file = await dispatch(getFileInfo({ uri: root.join(share.name ?? "").toString() }));
|
||||
} else {
|
||||
file = await dispatch(getFileInfo({ uri: share.source_uri ?? "" }));
|
||||
}
|
||||
return { share, file };
|
||||
};
|
||||
}
|
||||
29
src/redux/thunks/site.ts
Executable file
29
src/redux/thunks/site.ts
Executable file
@@ -0,0 +1,29 @@
|
||||
import { getSiteConfig } from "../../api/api.ts";
|
||||
import SessionManager from "../../session";
|
||||
import { applySetting } from "../siteConfigSlice.ts";
|
||||
import { AppThunk } from "../store.ts";
|
||||
|
||||
export function loadSiteConfig(section: string): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
const siteConfig = await dispatch(getSiteConfig(section));
|
||||
dispatch(
|
||||
applySetting({
|
||||
section: section,
|
||||
config: siteConfig,
|
||||
}),
|
||||
);
|
||||
localStorage.setItem(`siteConfigCache_${section}`, JSON.stringify(siteConfig));
|
||||
};
|
||||
}
|
||||
|
||||
export function updateSiteConfig(): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
await dispatch(loadSiteConfig("basic"));
|
||||
const {
|
||||
siteConfig: { basic },
|
||||
} = getState();
|
||||
if (basic.config.user) {
|
||||
SessionManager.updateUserIfExist(basic.config.user);
|
||||
}
|
||||
};
|
||||
}
|
||||
763
src/redux/thunks/viewer.ts
Executable file
763
src/redux/thunks/viewer.ts
Executable file
@@ -0,0 +1,763 @@
|
||||
import i18next from "i18next";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { getFileEntityUrl, getFileInfo, sendCreateViewerSession, sendUpdateFile } from "../../api/api.ts";
|
||||
import { FileResponse, Metadata, Viewer, ViewerAction, ViewerType } from "../../api/explorer.ts";
|
||||
import { AppError, Code } from "../../api/request.ts";
|
||||
import { DefaultCloseAction } from "../../component/Common/Snackbar/snackbar.tsx";
|
||||
import { canUpdate, getActionOpt } from "../../component/FileManager/ContextMenu/useActionDisplayOpt.ts";
|
||||
import { FileManagerIndex } from "../../component/FileManager/FileManager.tsx";
|
||||
import SessionManager, { UserSettings } from "../../session";
|
||||
import { isTrueVal } from "../../session/utils.ts";
|
||||
import { dataUrlToBytes, fileExtension, fileNameNoExt, getFileLinkedUri, sizeToString } from "../../util";
|
||||
import { base64Encode } from "../../util/base64.ts";
|
||||
import CrUri, { CrUriPrefix } from "../../util/uri.ts";
|
||||
import { closeContextMenu, ContextMenuTypes, fileUpdated } from "../fileManagerSlice.ts";
|
||||
import {
|
||||
closeImageEditor,
|
||||
setArchiveViewer,
|
||||
setCodeViewer,
|
||||
setCustomViewer,
|
||||
setDrawIOViewer,
|
||||
setEpubViewer,
|
||||
setExcalidrawViewer,
|
||||
setImageEditor,
|
||||
setImageViewer,
|
||||
setMarkdownViewer,
|
||||
setMusicPlayer,
|
||||
setPdfViewer,
|
||||
setPhotopeaViewer,
|
||||
setSearchPopup,
|
||||
setSidebar,
|
||||
setVideoViewer,
|
||||
setViewerSelector,
|
||||
setWopiViewer,
|
||||
} from "../globalStateSlice.ts";
|
||||
import { Viewers, ViewersByID } from "../siteConfigSlice.ts";
|
||||
import { AppThunk } from "../store.ts";
|
||||
import { askSaveAs, askStaleVersionAction } from "./dialog.ts";
|
||||
import { longRunningTaskWithSnackbar, refreshSingleFileSymbolicLinks } from "./file.ts";
|
||||
import { uploadRawFile } from "./filemanager.ts";
|
||||
|
||||
export interface ExpandedViewerSetting {
|
||||
[key: string]: Viewer[];
|
||||
}
|
||||
|
||||
export const builtInViewers = {
|
||||
image: "image",
|
||||
photopea: "photopea",
|
||||
monaco: "monaco",
|
||||
drawio: "drawio",
|
||||
markdown: "markdown",
|
||||
video: "video",
|
||||
pdf: "pdf",
|
||||
epub: "epub",
|
||||
music: "music",
|
||||
excalidraw: "excalidraw",
|
||||
archive: "archive",
|
||||
};
|
||||
|
||||
export function openViewers(
|
||||
index: number,
|
||||
file: FileResponse,
|
||||
size?: number,
|
||||
preferredVersion?: string,
|
||||
ignorePreference?: boolean,
|
||||
): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch(closeContextMenu({ index, value: undefined }));
|
||||
const {
|
||||
siteConfig: {
|
||||
explorer: { typed },
|
||||
},
|
||||
} = getState();
|
||||
|
||||
const ext = fileExtension(file.name) ?? "";
|
||||
const entitySize = size ?? file.size;
|
||||
|
||||
// Try user preference
|
||||
const userPreference = SessionManager.get(UserSettings.OpenWithPrefix + ext);
|
||||
if (!ignorePreference && userPreference && ViewersByID[userPreference]) {
|
||||
dispatch(openViewer(file, ViewersByID[userPreference], entitySize, preferredVersion));
|
||||
return;
|
||||
}
|
||||
|
||||
const viewerOptions = Viewers[ext];
|
||||
|
||||
if (!ignorePreference && viewerOptions.length == 1) {
|
||||
dispatch(openViewer(file, viewerOptions[0], entitySize, preferredVersion));
|
||||
return;
|
||||
}
|
||||
|
||||
// open viewer selection dialog
|
||||
dispatch(
|
||||
setViewerSelector({
|
||||
open: true,
|
||||
file,
|
||||
entitySize,
|
||||
viewers: viewerOptions,
|
||||
version: preferredVersion,
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function openViewer(
|
||||
file: FileResponse,
|
||||
viewer: Viewer,
|
||||
size: number,
|
||||
preferredVersion?: string,
|
||||
forceNotOpenInNew?: boolean,
|
||||
): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
if (!forceNotOpenInNew && viewer.type != ViewerType.custom && isTrueVal(viewer.props?.openInNew ?? "")) {
|
||||
const currentUrl = new URL(window.location.href);
|
||||
currentUrl.searchParams.set("viewer", viewer.id ?? "");
|
||||
currentUrl.searchParams.set("version", preferredVersion ?? "");
|
||||
currentUrl.searchParams.set("open", file.id ?? "");
|
||||
currentUrl.searchParams.set("size", size.toString());
|
||||
window.open(currentUrl.toString(), "_blank");
|
||||
return;
|
||||
}
|
||||
|
||||
// Warning for large file
|
||||
if (viewer.max_size && size > viewer.max_size) {
|
||||
enqueueSnackbar({
|
||||
message: i18next.t("fileManager.viewerFileSizeWarning", {
|
||||
file_size: sizeToString(size),
|
||||
max: sizeToString(viewer.max_size),
|
||||
app: i18next.t(viewer.display_name),
|
||||
}),
|
||||
variant: "warning",
|
||||
action: DefaultCloseAction,
|
||||
});
|
||||
}
|
||||
|
||||
const isSharedFile = file.metadata?.[Metadata.share_redirect] ?? false;
|
||||
const originalFileId = file.id;
|
||||
|
||||
if (isSharedFile) {
|
||||
file = await dispatch(refreshSingleFileSymbolicLinks(file));
|
||||
}
|
||||
|
||||
if (viewer.type == ViewerType.builtin) {
|
||||
let primaryEntity = file.primary_entity;
|
||||
if (isSharedFile) {
|
||||
const fileInfo = await dispatch(getFileInfo({ uri: getFileLinkedUri(file) }));
|
||||
primaryEntity = fileInfo.primary_entity;
|
||||
}
|
||||
switch (viewer.id) {
|
||||
case builtInViewers.image: {
|
||||
// open image viewer
|
||||
const fm = getState().fileManager[FileManagerIndex.main];
|
||||
const fileIndex = fm.list?.files?.findIndex((f) => f.id == originalFileId);
|
||||
dispatch(setSearchPopup(false));
|
||||
dispatch(
|
||||
setImageViewer({
|
||||
open: true,
|
||||
index: fileIndex,
|
||||
file,
|
||||
exts: viewer.exts,
|
||||
version: preferredVersion,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case builtInViewers.photopea:
|
||||
dispatch(
|
||||
setPhotopeaViewer({
|
||||
open: true,
|
||||
file,
|
||||
version: preferredVersion ?? primaryEntity,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case builtInViewers.monaco:
|
||||
dispatch(
|
||||
setCodeViewer({
|
||||
open: true,
|
||||
file,
|
||||
version: preferredVersion ?? primaryEntity,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case builtInViewers.drawio:
|
||||
dispatch(
|
||||
setDrawIOViewer({
|
||||
open: true,
|
||||
file,
|
||||
version: preferredVersion,
|
||||
host: viewer.props?.host,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case builtInViewers.markdown:
|
||||
dispatch(
|
||||
setMarkdownViewer({
|
||||
open: true,
|
||||
file,
|
||||
version: preferredVersion ?? primaryEntity,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case builtInViewers.excalidraw:
|
||||
dispatch(
|
||||
setExcalidrawViewer({
|
||||
open: true,
|
||||
file,
|
||||
version: preferredVersion ?? primaryEntity,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case builtInViewers.video:
|
||||
dispatch(
|
||||
setVideoViewer({
|
||||
open: true,
|
||||
file,
|
||||
version: preferredVersion,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case builtInViewers.pdf:
|
||||
dispatch(
|
||||
setPdfViewer({
|
||||
open: true,
|
||||
file,
|
||||
version: preferredVersion,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case builtInViewers.epub:
|
||||
dispatch(
|
||||
setEpubViewer({
|
||||
open: true,
|
||||
file,
|
||||
version: preferredVersion,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case builtInViewers.archive:
|
||||
dispatch(
|
||||
setArchiveViewer({
|
||||
open: true,
|
||||
file,
|
||||
version: preferredVersion,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case builtInViewers.music: {
|
||||
// open image viewer
|
||||
const fm = getState().fileManager[FileManagerIndex.main];
|
||||
let fileIndex = -1;
|
||||
let files: FileResponse[] = [];
|
||||
if (preferredVersion) {
|
||||
fileIndex = 0;
|
||||
files = [file];
|
||||
} else {
|
||||
fm.list?.files?.forEach((f) => {
|
||||
if (f.id == originalFileId) {
|
||||
fileIndex = files.length;
|
||||
f = { ...file };
|
||||
}
|
||||
|
||||
if (viewer.exts.indexOf(fileExtension(f.name) ?? "") > -1) {
|
||||
files.push(f);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (fileIndex >= 0) {
|
||||
dispatch(
|
||||
setMusicPlayer({
|
||||
files: files,
|
||||
startIndex: fileIndex,
|
||||
version: preferredVersion,
|
||||
}),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (viewer.type == ViewerType.wopi) {
|
||||
return dispatch(openWopiViewer(file, viewer, preferredVersion));
|
||||
} else if (viewer.type == ViewerType.custom) {
|
||||
return dispatch(openCustomViewer(file, viewer, preferredVersion));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function openCustomViewer(file: FileResponse, viewer: Viewer, preferredVersion?: string): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
const entityUrl = await longRunningTaskWithSnackbar(
|
||||
dispatch(
|
||||
getFileEntityUrl({
|
||||
uris: [getFileLinkedUri(file)],
|
||||
entity: preferredVersion,
|
||||
use_primary_site_url: true,
|
||||
}),
|
||||
),
|
||||
"fileManager.preparingOpenFile",
|
||||
);
|
||||
|
||||
const currentUser = SessionManager.currentUser();
|
||||
|
||||
const vars: { [key: string]: string } = {
|
||||
src: encodeURIComponent(entityUrl.urls[0].url),
|
||||
src_raw: entityUrl.urls[0].url,
|
||||
src_raw_base64: base64Encode(entityUrl.urls[0].url),
|
||||
name: encodeURIComponent(file.name),
|
||||
version: preferredVersion ? preferredVersion : "",
|
||||
id: file.id,
|
||||
user_id: currentUser?.id ?? "",
|
||||
user_display_name: encodeURIComponent(currentUser?.nickname ?? ""),
|
||||
};
|
||||
|
||||
// replace variables in viewer.url
|
||||
let url = viewer.url;
|
||||
if (!url) {
|
||||
console.error("Viewer URL not set");
|
||||
return;
|
||||
}
|
||||
for (const key in vars) {
|
||||
url = url.replace(`{$${key}}`, vars[key]);
|
||||
}
|
||||
|
||||
// if url matches custom scheme pattern like nplayer://xxx, use window.location.assign
|
||||
if (/^(?!https?:\/\/)[a-zA-Z0-9]+:\/\//.test(url)) {
|
||||
window.location.assign(url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTrueVal(viewer.props?.openInNew ?? "")) {
|
||||
window.window.open(url);
|
||||
return;
|
||||
}
|
||||
|
||||
// open viewer
|
||||
dispatch(setCustomViewer({ open: true, url, file, version: preferredVersion }));
|
||||
};
|
||||
}
|
||||
|
||||
export function openWopiViewer(file: FileResponse, viewer: Viewer, preferredVersion?: string): AppThunk {
|
||||
return async (dispatch, _getState) => {
|
||||
const displayOpt = getActionOpt([file], Viewers, ContextMenuTypes.file);
|
||||
const action = !preferredVersion && canUpdate(displayOpt) ? ViewerAction.edit : ViewerAction.view;
|
||||
|
||||
const viewerSession = await longRunningTaskWithSnackbar(
|
||||
dispatch(
|
||||
sendCreateViewerSession({
|
||||
uri: getFileLinkedUri(file),
|
||||
viewer_id: viewer.id,
|
||||
preferred_action: action,
|
||||
version: preferredVersion,
|
||||
}),
|
||||
),
|
||||
"fileManager.preparingOpenFile",
|
||||
);
|
||||
|
||||
if (!viewerSession.wopi_src) {
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
setWopiViewer({
|
||||
open: true,
|
||||
src: viewerSession.wopi_src,
|
||||
session: viewerSession.session,
|
||||
file: file,
|
||||
version: preferredVersion,
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function onImageViewerIndexChange(file: FileResponse): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
const {
|
||||
globalState: { sidebarOpen },
|
||||
} = getState();
|
||||
if (sidebarOpen) {
|
||||
dispatch(
|
||||
setSidebar({
|
||||
open: true,
|
||||
target: file,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function switchToImageEditor(file: FileResponse, version?: string): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
const {
|
||||
globalState: { imageViewer },
|
||||
} = getState();
|
||||
if (!imageViewer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isSharedFile = file.metadata?.[Metadata.share_redirect] ?? false;
|
||||
if (isSharedFile) {
|
||||
const fileInfo = await dispatch(getFileInfo({ uri: getFileLinkedUri(file) }));
|
||||
version = fileInfo.primary_entity;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
setImageViewer({
|
||||
...imageViewer,
|
||||
open: false,
|
||||
}),
|
||||
);
|
||||
|
||||
dispatch(
|
||||
setImageEditor({
|
||||
open: true,
|
||||
file,
|
||||
version: version ?? file.primary_entity,
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function switchToImageViewer(): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
const {
|
||||
globalState: { imageViewer },
|
||||
} = getState();
|
||||
dispatch(closeImageEditor());
|
||||
if (imageViewer) {
|
||||
dispatch(setImageViewer({ ...imageViewer, open: true }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function saveImage(name: string, data: string, file: FileResponse, version?: string): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
if (!version) {
|
||||
version = file.primary_entity;
|
||||
}
|
||||
|
||||
const isSharedFile = file.metadata?.[Metadata.share_redirect] ?? false;
|
||||
let originFileUri = new CrUri(getFileLinkedUri(file));
|
||||
if (name != file.name) {
|
||||
if (isSharedFile) {
|
||||
// For symbolic link, we need to save to the same folder as the link
|
||||
originFileUri = new CrUri(file.path);
|
||||
}
|
||||
|
||||
originFileUri = originFileUri.parent().join(name);
|
||||
}
|
||||
|
||||
const savedImageFile = await dispatch(saveFile(originFileUri.toString(), await dataUrlToBytes(data), version));
|
||||
if (savedImageFile) {
|
||||
const {
|
||||
globalState: { imageViewer },
|
||||
} = getState();
|
||||
if (imageViewer) {
|
||||
dispatch(
|
||||
setImageViewer({
|
||||
...imageViewer,
|
||||
file: name != file.name ? file : savedImageFile,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function saveFile(
|
||||
uri: string,
|
||||
data: any,
|
||||
version?: string,
|
||||
saveAsNew?: boolean,
|
||||
ignoreSnackbar?: boolean,
|
||||
): AppThunk<Promise<FileResponse | undefined>> {
|
||||
return async (dispatch, _getState): Promise<FileResponse | undefined> => {
|
||||
let savedFile: FileResponse | undefined;
|
||||
if (saveAsNew) {
|
||||
try {
|
||||
const fileName = new CrUri(uri).elements().pop();
|
||||
if (fileName) {
|
||||
const saveAsDst = await dispatch(askSaveAs(fileName));
|
||||
const dst = new CrUri(saveAsDst.uri).join(saveAsDst.name);
|
||||
uri = dst.toString();
|
||||
}
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
savedFile = await dispatch(
|
||||
sendUpdateFile(
|
||||
{
|
||||
uri: uri,
|
||||
previous: version,
|
||||
},
|
||||
data,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof AppError && e.code == Code.StaleVersion) {
|
||||
// Handle version conflict
|
||||
try {
|
||||
const opt = await dispatch(askStaleVersionAction(uri));
|
||||
if (opt.overwrite) {
|
||||
return await dispatch(saveFile(uri, data));
|
||||
} else if (opt.saveAs) {
|
||||
return await dispatch(saveFile(opt.saveAs, data, version));
|
||||
}
|
||||
} catch (e) {
|
||||
// Cancel save action
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!savedFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ignoreSnackbar) {
|
||||
enqueueSnackbar({
|
||||
message: i18next.t("fileManager.fileSaved"),
|
||||
variant: "success",
|
||||
action: DefaultCloseAction,
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(
|
||||
fileUpdated({
|
||||
index: FileManagerIndex.main,
|
||||
value: [
|
||||
{
|
||||
oldPath: uri,
|
||||
file: savedFile,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
return savedFile;
|
||||
};
|
||||
}
|
||||
|
||||
export function savePhotopea(data: ArrayBuffer, file: FileResponse, version?: string, saveAsNew?: boolean): AppThunk {
|
||||
return async (dispatch, getState) => {
|
||||
if (!version) {
|
||||
version = file.primary_entity;
|
||||
}
|
||||
const savedFile = await dispatch(saveFile(getFileLinkedUri(file), data, version, saveAsNew));
|
||||
|
||||
if (savedFile) {
|
||||
const {
|
||||
globalState: { photopeaViewer },
|
||||
} = getState();
|
||||
if (photopeaViewer) {
|
||||
dispatch(
|
||||
setPhotopeaViewer({
|
||||
...photopeaViewer,
|
||||
file: savedFile,
|
||||
version: savedFile.primary_entity,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function saveCode(
|
||||
data: string,
|
||||
file: FileResponse,
|
||||
version?: string,
|
||||
saveAsNew?: boolean,
|
||||
): AppThunk<Promise<void>> {
|
||||
return async (dispatch, getState) => {
|
||||
const isLinkedFile = file.metadata?.[Metadata.share_redirect] ?? false;
|
||||
if (!version && !isLinkedFile) {
|
||||
version = file.primary_entity;
|
||||
}
|
||||
const savedFile = await dispatch(saveFile(getFileLinkedUri(file), data, version, saveAsNew));
|
||||
|
||||
if (savedFile) {
|
||||
const {
|
||||
globalState: { codeViewer },
|
||||
} = getState();
|
||||
if (codeViewer) {
|
||||
dispatch(
|
||||
setCodeViewer({
|
||||
...codeViewer,
|
||||
file: savedFile,
|
||||
version: savedFile.primary_entity,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function saveDrawIO(
|
||||
data: string,
|
||||
file: FileResponse,
|
||||
saveAsNew?: boolean,
|
||||
): AppThunk<Promise<FileResponse | undefined>> {
|
||||
return async (dispatch, getState): Promise<FileResponse | undefined> => {
|
||||
const savedFile = await dispatch(saveFile(getFileLinkedUri(file), data, undefined, saveAsNew, true));
|
||||
|
||||
if (savedFile) {
|
||||
const {
|
||||
globalState: { drawIOViewer },
|
||||
} = getState();
|
||||
if (drawIOViewer) {
|
||||
dispatch(
|
||||
setDrawIOViewer({
|
||||
...drawIOViewer,
|
||||
file: savedFile,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return savedFile;
|
||||
};
|
||||
}
|
||||
|
||||
export function saveExcalidraw(
|
||||
data: string,
|
||||
file: FileResponse,
|
||||
version?: string,
|
||||
saveAsNew?: boolean,
|
||||
): AppThunk<Promise<void>> {
|
||||
return async (dispatch, getState) => {
|
||||
const isLinkedFile = file.metadata?.[Metadata.share_redirect] ?? false;
|
||||
if (!version && !isLinkedFile) {
|
||||
version = file.primary_entity;
|
||||
}
|
||||
const savedFile = await dispatch(saveFile(getFileLinkedUri(file), data, version, saveAsNew));
|
||||
|
||||
if (savedFile) {
|
||||
const {
|
||||
globalState: { excalidrawViewer },
|
||||
} = getState();
|
||||
if (excalidrawViewer) {
|
||||
dispatch(
|
||||
setExcalidrawViewer({
|
||||
...excalidrawViewer,
|
||||
file: savedFile,
|
||||
version: savedFile.primary_entity,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function saveMarkdown(
|
||||
data: string,
|
||||
file: FileResponse,
|
||||
version?: string,
|
||||
saveAsNew?: boolean,
|
||||
): AppThunk<Promise<void>> {
|
||||
return async (dispatch, getState) => {
|
||||
const isLinkedFile = file.metadata?.[Metadata.share_redirect] ?? false;
|
||||
if (!version && !isLinkedFile) {
|
||||
version = file.primary_entity;
|
||||
}
|
||||
const savedFile = await dispatch(saveFile(getFileLinkedUri(file), data, version, saveAsNew));
|
||||
|
||||
if (savedFile) {
|
||||
const {
|
||||
globalState: { markdownViewer },
|
||||
} = getState();
|
||||
if (markdownViewer) {
|
||||
dispatch(
|
||||
setMarkdownViewer({
|
||||
...markdownViewer,
|
||||
file: savedFile,
|
||||
version: savedFile.primary_entity,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const subtitleSuffix = ["ass", "srt", "vrr"];
|
||||
export function findSubtitleOptions(): AppThunk<FileResponse[]> {
|
||||
return (_dispatch, getState): FileResponse[] => {
|
||||
const {
|
||||
globalState: { videoViewer },
|
||||
fileManager,
|
||||
} = getState();
|
||||
if (!videoViewer || !videoViewer.file) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const fm = fileManager[FileManagerIndex.main];
|
||||
|
||||
const fileNameMatch = fileNameNoExt(videoViewer.file.name) + ".";
|
||||
const options = fm.list?.files
|
||||
.filter((f) => {
|
||||
return subtitleSuffix.indexOf(fileExtension(f.name) ?? "") !== -1;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return a.name.startsWith(fileNameMatch) && !b.name.startsWith(fileNameMatch) ? -1 : 0;
|
||||
});
|
||||
|
||||
return options ?? [];
|
||||
};
|
||||
}
|
||||
|
||||
const BROKEN_IMG_URI =
|
||||
"data:image/svg+xml;charset=utf-8," +
|
||||
encodeURIComponent(/* xml */ `
|
||||
<svg id="imgLoadError" xmlns="http://www.w3.org/2000/svg" width="100" height="100">
|
||||
<rect x="0" y="0" width="100" height="100" fill="none" stroke="red" stroke-width="4" stroke-dasharray="4" />
|
||||
<text x="50" y="55" text-anchor="middle" font-size="20" fill="red">⚠️</text>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
export function markdownImagePreviewHandler(imageSource: string, mdFileUri: string): AppThunk<Promise<string>> {
|
||||
return async (dispatch, getState) => {
|
||||
// For URl, return the image source
|
||||
if (imageSource.startsWith("http://") || imageSource.startsWith("https://")) {
|
||||
return imageSource;
|
||||
}
|
||||
|
||||
let uri = new CrUri(mdFileUri)?.parent();
|
||||
if (imageSource.startsWith(CrUriPrefix)) {
|
||||
uri = new CrUri(imageSource);
|
||||
} else if (uri) {
|
||||
uri = uri.join_raw(imageSource);
|
||||
} else {
|
||||
return imageSource;
|
||||
}
|
||||
|
||||
try {
|
||||
const file = await dispatch(getFileInfo({ uri: uri.toString() }, true));
|
||||
const fileUrl = await dispatch(getFileEntityUrl({ uris: [getFileLinkedUri(file)] }));
|
||||
return fileUrl.urls[0].url;
|
||||
} catch (e) {
|
||||
return BROKEN_IMG_URI;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function markdownImageAutocompleteSuggestions(): AppThunk<string[] | null> {
|
||||
return (_dispatch, getState) => {
|
||||
const files = getState().fileManager[FileManagerIndex.main]?.list?.files;
|
||||
if (!files) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const suggestions = files.filter((f) => {
|
||||
const ext = fileExtension(f.name);
|
||||
return ViewersByID[builtInViewers.image]?.exts.indexOf(ext ?? "") !== -1;
|
||||
});
|
||||
|
||||
return suggestions.map((f) => f.name);
|
||||
};
|
||||
}
|
||||
|
||||
export function uploadMarkdownImage(file: File): AppThunk<Promise<string>> {
|
||||
return async (dispatch, getState) => {
|
||||
const task = await dispatch(uploadRawFile(file));
|
||||
return task.name;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user