├── .env ├── src ├── react-app-env.d.ts ├── Components │ ├── FileList │ │ ├── FileListSublist │ │ │ ├── FileListSublist.css │ │ │ └── FileListSublist.tsx │ │ ├── FileListEmptyMessage.css │ │ ├── FileListEmptyMessage.tsx │ │ ├── FileList.css │ │ └── FileList.tsx │ ├── Breadcrumb │ │ ├── BreadcrumbText.css │ │ └── BreadcrumbText.tsx │ ├── Dialogs │ │ ├── dialogTypes.ts │ │ ├── Dialogs.tsx │ │ ├── Move │ │ │ └── Move.tsx │ │ ├── Copy │ │ │ └── Copy.tsx │ │ ├── Content │ │ │ └── Content.tsx │ │ ├── CreateFile │ │ │ └── CreateFile.tsx │ │ ├── CreateFolder │ │ │ └── CreateFolder.tsx │ │ ├── Media │ │ │ └── Media.tsx │ │ ├── Rename │ │ │ └── Rename.tsx │ │ ├── UploadFile │ │ │ └── UploadFile.tsx │ │ ├── Settings │ │ │ └── Settings.tsx │ │ ├── ChooseLocation │ │ │ └── ChooseLocation.tsx │ │ ├── Edit │ │ │ └── Edit.tsx │ │ └── Menu │ │ │ └── Menu.tsx │ ├── File │ │ ├── File.css │ │ ├── FileSublist │ │ │ └── FileSublist.tsx │ │ └── File.tsx │ ├── ContextMenu │ │ ├── ContextMenu.css │ │ ├── ContextMenuActions │ │ │ ├── CopyAction.tsx │ │ │ ├── RenameAction.tsx │ │ │ ├── MoveAction.tsx │ │ │ ├── RemoveAction.tsx │ │ │ ├── ZipAction.tsx │ │ │ ├── EditAction.tsx │ │ │ ├── ExtractAction.tsx │ │ │ ├── OpenInNewTabAction.tsx │ │ │ ├── SettingsAction.tsx │ │ │ ├── UploadFileAction.tsx │ │ │ ├── CreateFileAction.tsx │ │ │ ├── CreateFolderAction.tsx │ │ │ ├── ChooseLocationAction.tsx │ │ │ ├── DownloadAction.tsx │ │ │ └── OpenAction.tsx │ │ └── ContextMenu.tsx │ ├── Loader │ │ └── Loader.tsx │ ├── FileUploader │ │ ├── UploadFileList.tsx │ │ └── FileUploader.tsx │ ├── Navbar │ │ ├── ThreeDotsMenu.tsx │ │ └── Navbar.tsx │ ├── Notification │ │ ├── DynamicSnackbar.tsx │ │ └── NotificationBar.tsx │ └── HistoryHandler │ │ └── HistoryHandler.tsx ├── Api │ ├── types.ts │ ├── ApiCache.ts │ ├── contentTypes.ts │ ├── Item.ts │ └── ApiHandler.ts ├── Reducers │ ├── errorReducer.ts │ ├── loadingReducer.ts │ ├── currentBlobReducer.ts │ ├── pathReducer.ts │ ├── uploadReducer.ts │ ├── reducer.ts │ ├── settingsReducer.ts │ ├── accountReducer.ts │ ├── dialogsReducer.ts │ └── itemsReducer.ts ├── App.test.tsx ├── config.ts ├── index.css ├── index.tsx ├── Actions │ ├── actionTypes.ts │ └── Actions.ts ├── App.tsx ├── types │ └── solid-file-client.d.ts └── serviceWorker.ts ├── public ├── favicon.ico ├── 404.md ├── manifest.json ├── index.html └── vendor │ └── plyr │ └── plyr.svg ├── images └── Screenshot.png ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ ├── test_setup_spec.js │ ├── file_spec.js │ └── zip_spec.js ├── support │ ├── index.js │ └── commands.js └── plugins │ └── index.js ├── delete_css_storage.js ├── .gitignore ├── tsconfig.json ├── .github └── workflows │ └── e2e.yml ├── README.md └── package.json /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_VERSION=$npm_package_version -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Otto-AA/solid-filemanager/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /images/Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Otto-AA/solid-filemanager/HEAD/images/Screenshot.png -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3000", 3 | "experimentalSessionAndOrigin": true 4 | } 5 | -------------------------------------------------------------------------------- /src/Components/FileList/FileListSublist/FileListSublist.css: -------------------------------------------------------------------------------- 1 | .FileListSublist { 2 | overflow: auto; 3 | max-height: 20em; 4 | } -------------------------------------------------------------------------------- /src/Api/types.ts: -------------------------------------------------------------------------------- 1 | import { FileItem, FolderItem } from "./Item"; 2 | 3 | export interface FolderItems { 4 | files: FileItem[], 5 | folders: FolderItem[] 6 | }; -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /delete_css_storage.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | // delete community-solid-server storage 4 | // as it adds new account on every test 5 | fs.rmSync('./community-solid-server', { recursive: true, force: true }); -------------------------------------------------------------------------------- /src/Components/Breadcrumb/BreadcrumbText.css: -------------------------------------------------------------------------------- 1 | .BreadcrumbText { 2 | } 3 | 4 | .BreadcrumbText span { 5 | cursor: pointer; 6 | text-overflow: ellipsis; 7 | } 8 | 9 | .BreadcrumbText span:hover { 10 | color: #efefef; 11 | } -------------------------------------------------------------------------------- /src/Components/FileList/FileListEmptyMessage.css: -------------------------------------------------------------------------------- 1 | .FileListEmptyMessage { 2 | margin: 5px 10px; 3 | padding: 20px; 4 | display: block; 5 | border-radius: 5px; 6 | background: #efefef; 7 | color: #333; 8 | font-size: 15px; 9 | } -------------------------------------------------------------------------------- /public/404.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /404.html 3 | --- 4 | # 404 - Not found 5 | The requested site was not found. 6 | 7 | You can go back to the github repository [here](https://github.com/Otto-AA/solid-filemanager), or open the file manager [here](https://otto-aa.github.io/solid-filemanager/). -------------------------------------------------------------------------------- /src/Components/FileList/FileListEmptyMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './FileListEmptyMessage.css'; 3 | 4 | export default function FileListEmptyMessage() { 5 | return ( 6 |
7 | No files in this folder 8 |
9 | ); 10 | }; -------------------------------------------------------------------------------- /cypress/integration/test_setup_spec.js: -------------------------------------------------------------------------------- 1 | describe('The Home Page', () => { 2 | it('can use createRandomAccount', () => { 3 | cy.createRandomAccount().then(console.log) 4 | }) 5 | 6 | it('can use login command', () => { 7 | cy.createRandomAccount().as('user') 8 | cy.get('@user').then(user => cy.login(user)) 9 | }) 10 | }) -------------------------------------------------------------------------------- /src/Reducers/errorReducer.ts: -------------------------------------------------------------------------------- 1 | import { Action, SET_ERROR_MESSAGE } from "../Actions/actionTypes"; 2 | 3 | export const errorMessage = (state = '', action: Action): typeof state => { 4 | switch(action.type) { 5 | case SET_ERROR_MESSAGE: 6 | return action.value; 7 | default: 8 | return state; 9 | } 10 | }; -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Solid Filemanager", 3 | "name": "Solid Filemanager", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#fff", 14 | "background_color": "#2196f3" 15 | } 16 | -------------------------------------------------------------------------------- /src/Components/Dialogs/dialogTypes.ts: -------------------------------------------------------------------------------- 1 | export interface DialogDispatchProps { 2 | handleClose(event: DialogButtonClickEvent): void; 3 | handleSubmit?(event: DialogButtonClickEvent, options?: Record): void; 4 | } 5 | 6 | export interface DialogStateProps { 7 | open: boolean; 8 | } 9 | 10 | export type DialogButtonClickEvent = React.MouseEvent; -------------------------------------------------------------------------------- /src/Components/File/File.css: -------------------------------------------------------------------------------- 1 | .File { 2 | cursor: pointer; 3 | float: left; 4 | display: block; 5 | width: 100%; 6 | user-select: none; 7 | } 8 | 9 | .File:hover { 10 | cursor: pointer; 11 | background: #fafafa; 12 | } 13 | 14 | .File[data-selected=true] { 15 | background-color: #e8f0fe; 16 | 17 | } 18 | 19 | .File[data-selected=true], 20 | .File[data-selected=true] > li > div > span { 21 | color: #1967d2; 22 | } -------------------------------------------------------------------------------- /src/Components/FileList/FileList.css: -------------------------------------------------------------------------------- 1 | .FileList { 2 | overflow: auto 3 | } 4 | 5 | .FileList .File .filename > span { 6 | text-overflow: ellipsis; 7 | overflow: hidden; 8 | white-space: nowrap; 9 | } 10 | 11 | @media (min-width : 600px) { 12 | .FileList .File { 13 | float: left; 14 | width: 33.3%; 15 | } 16 | } 17 | @media (min-width : 1024px) { 18 | .FileList .File { 19 | float: left; 20 | width: 25%; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Reducers/loadingReducer.ts: -------------------------------------------------------------------------------- 1 | import { Action, DISPLAY_LOADING, STOP_LOADING } from "../Actions/actionTypes"; 2 | 3 | const initialLoadingState: boolean = false; 4 | 5 | export const loading = (state = initialLoadingState, action: Action): boolean => { 6 | switch(action.type) { 7 | case DISPLAY_LOADING: 8 | return true; 9 | case STOP_LOADING: 10 | return false; 11 | default: 12 | return state; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenu.css: -------------------------------------------------------------------------------- 1 | .XXXXXXXXXXXContextMenu { 2 | max-width: 200px; 3 | position: absolute; 4 | display: block; 5 | background: green; 6 | border: 1px solid black; 7 | padding: 10px; 8 | text-align: left; 9 | } 10 | 11 | .ContextMenu ul { 12 | margin: 0; 13 | padding: 0; 14 | list-style: none; 15 | } 16 | 17 | .ContextMenu ul li { 18 | padding: 10px 20px; 19 | border-bottom: 1px solid #000; 20 | } 21 | 22 | .ContextMenu ul li:last-child { 23 | border: none; 24 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # build 9 | /build 10 | 11 | # testing 12 | /coverage 13 | /cypress/videos 14 | /cypress/screenshots 15 | /cypress/downloads 16 | 17 | # community-solid-server 18 | /community-solid-server 19 | 20 | # misc 21 | .DS_Store 22 | .env.local 23 | .env.development.local 24 | .env.test.local 25 | .env.production.local 26 | 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* -------------------------------------------------------------------------------- /src/Reducers/currentBlobReducer.ts: -------------------------------------------------------------------------------- 1 | import { Action, SET_LOADED_BLOB, RESET_LOADED_BLOB } from "../Actions/actionTypes"; 2 | 3 | export const blob = (state: string|null = null, action: Action): typeof state => { 4 | switch(action.type) { 5 | case SET_LOADED_BLOB: 6 | return URL.createObjectURL(action.value); 7 | case RESET_LOADED_BLOB: 8 | if (state !== null) 9 | URL.revokeObjectURL(state); 10 | return null; 11 | default: 12 | return state; 13 | } 14 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": false, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "preserve" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import thunk from 'redux-thunk'; 5 | import { Provider } from 'react-redux'; 6 | import { createStore, applyMiddleware } from 'redux' 7 | import reducer from './Reducers/reducer' 8 | 9 | const store = createStore(reducer, applyMiddleware(thunk)); 10 | 11 | it('renders without crashing', () => { 12 | const div = document.createElement('div'); 13 | ReactDOM.render( 14 | 15 | 16 | 17 | , div); 18 | ReactDOM.unmountComponentAtNode(div); 19 | }); 20 | -------------------------------------------------------------------------------- /src/Reducers/pathReducer.ts: -------------------------------------------------------------------------------- 1 | import { Action, ENTER_FOLDER, SET_PATH, MOVE_FOLDER_UPWARDS } from "../Actions/actionTypes"; 2 | 3 | 4 | const initialPath: string[] = []; 5 | 6 | export const path = (state = initialPath, action: Action): typeof initialPath => { 7 | switch(action.type) { 8 | case ENTER_FOLDER: 9 | return [...state, action.value]; 10 | case MOVE_FOLDER_UPWARDS: 11 | return action.value > 0 ? 12 | state.slice(0, -action.value) 13 | : state; 14 | case SET_PATH: 15 | return [...action.value]; 16 | default: 17 | return state; 18 | } 19 | }; -------------------------------------------------------------------------------- /src/Reducers/uploadReducer.ts: -------------------------------------------------------------------------------- 1 | import { Action, SET_UPLOAD_FILE_LIST, SET_UPLOAD_FILE_PROGRESS } from "../Actions/actionTypes"; 2 | 3 | const initialUploadState = { 4 | fileList: null as FileList|null, 5 | progress: 0, 6 | }; 7 | 8 | export const upload = (state = initialUploadState, action: Action): typeof initialUploadState => { 9 | switch(action.type) { 10 | case SET_UPLOAD_FILE_LIST: 11 | return { ...state, fileList: action.value as FileList }; 12 | case SET_UPLOAD_FILE_PROGRESS: 13 | return { ...state, progress: action.value as number }; 14 | default: 15 | return state; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | 8 | jobs: 9 | e2e: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: '22' 18 | cache: 'npm' 19 | 20 | - name: Install dependencies 21 | run: npm ci 22 | - run: npm run test:e2e 23 | 24 | - name: Archive test videos 25 | if: always() 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: cypress-artifacts 29 | path: | 30 | cypress/screenshots 31 | cypress/videos 32 | retention-days: 3 -------------------------------------------------------------------------------- /src/Components/Loader/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withStyles, createStyles, Theme, WithStyles } from '@material-ui/core/styles'; 3 | import CircularProgress from '@material-ui/core/CircularProgress'; 4 | import Grid from '@material-ui/core/Grid'; 5 | 6 | const styles = (theme: Theme) => createStyles({ 7 | progress: { 8 | margin: theme.spacing() * 10, 9 | }, 10 | }); 11 | 12 | function Loader(props: LoaderProps) { 13 | return ( 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | interface LoaderProps extends WithStyles {}; 21 | 22 | export default withStyles(styles)(Loader); 23 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /src/Reducers/reducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { account } from './accountReducer'; 3 | import { blob } from './currentBlobReducer'; 4 | import { visibleDialogs, contextMenu } from './dialogsReducer'; 5 | import { errorMessage } from './errorReducer'; 6 | import { items } from './itemsReducer'; 7 | import { loading } from './loadingReducer'; 8 | import { path } from './pathReducer'; 9 | import { upload } from './uploadReducer'; 10 | import { settings } from './settingsReducer'; 11 | 12 | const rootReducer = combineReducers({ 13 | account, 14 | blob, 15 | contextMenu, 16 | visibleDialogs, 17 | errorMessage, 18 | items, 19 | loading, 20 | path, 21 | settings, 22 | upload, 23 | }); 24 | 25 | export default rootReducer; 26 | export type AppState = ReturnType; -------------------------------------------------------------------------------- /src/Reducers/settingsReducer.ts: -------------------------------------------------------------------------------- 1 | import { Action, TOGGLE_WITH_ACL, TOGGLE_WITH_META } from "../Actions/actionTypes"; 2 | import config from "../config"; 3 | 4 | interface SettingsState { 5 | withAcl: boolean; 6 | withMeta: boolean; 7 | } 8 | 9 | const initialState: SettingsState = { 10 | withAcl: config.withAcl(), 11 | withMeta: config.withMeta(), 12 | }; 13 | 14 | export const settings = (state = initialState, action: Action): SettingsState => { 15 | switch(action.type) { 16 | case TOGGLE_WITH_ACL: 17 | config.toggleWithAcl(); 18 | return { ...state, withAcl: config.withAcl() } 19 | case TOGGLE_WITH_META: 20 | config.toggleWithMeta(); 21 | return { ...state, withMeta: config.withMeta() } 22 | default: 23 | return state; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | // eslint-disable-next-line no-unused-vars 19 | module.exports = (on, config) => { 20 | // `on` is used to hook into various events Cypress emits 21 | // `config` is the resolved Cypress config 22 | } 23 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | let host: string | null = null; 2 | let withAcl: boolean = localStorage.getItem('config_with_acl') === 'true'; 3 | let withMeta: boolean = localStorage.getItem('config_with_meta') === 'true'; 4 | 5 | const config = { 6 | getHost() { 7 | return host; 8 | }, 9 | setHost(newHost: string) { 10 | host = newHost; 11 | while (host.endsWith('/')) 12 | host = host.slice(0, -1); 13 | }, 14 | withAcl() { 15 | return withAcl; 16 | }, 17 | toggleWithAcl() { 18 | withAcl = !withAcl 19 | localStorage.setItem('config_with_acl', withAcl ? 'true' : 'false') 20 | }, 21 | withMeta() { 22 | return withMeta; 23 | }, 24 | toggleWithMeta() { 25 | withMeta = !withMeta 26 | localStorage.setItem('config_with_meta', withMeta ? 'true' : 'false') 27 | }, 28 | } 29 | 30 | export default config; 31 | 32 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Roboto", "Segoe UI", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | } 8 | 9 | code { 10 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 11 | } 12 | 13 | *::-webkit-scrollbar-track { 14 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); 15 | border-radius: 0; 16 | background-color: #F5F5F5; 17 | cursor: default; 18 | } 19 | 20 | *::-webkit-scrollbar { 21 | width: 8px; 22 | background-color: #F5F5F5; 23 | cursor: default; 24 | } 25 | 26 | *::-webkit-scrollbar-thumb { 27 | border-radius: 0px; 28 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); 29 | background-color: #555; 30 | cursor: default; 31 | } 32 | -------------------------------------------------------------------------------- /src/Api/ApiCache.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | import { Item } from './Item'; 3 | 4 | export default class ApiCache { 5 | _data: Record = {}; 6 | 7 | /** 8 | * Add data to the cache 9 | */ 10 | add(path: string, itemList: Item[]): Item[] { 11 | this._data[path] = itemList; 12 | return itemList; 13 | } 14 | 15 | /** 16 | * Return true if the url is already cached 17 | */ 18 | contains(path: string): boolean { 19 | return Object.keys(this._data).includes(path); 20 | } 21 | 22 | /** 23 | * Get the cached data 24 | */ 25 | get(path: string): Item[] { 26 | return this._data[path]; 27 | } 28 | 29 | /** 30 | * Remove paths from the cache 31 | */ 32 | remove(...paths: string[]) { 33 | paths.filter(path => this.contains(path)) 34 | .forEach(path => delete this._data[path]); 35 | } 36 | 37 | /** 38 | * Clear the whole cache 39 | */ 40 | clear() { 41 | this._data = {}; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Components/FileUploader/UploadFileList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import List from '@material-ui/core/List'; 3 | import ListItem from '@material-ui/core/ListItem'; 4 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 5 | import ListItemText from '@material-ui/core/ListItemText'; 6 | import FileIcon from '@material-ui/icons/InsertDriveFile'; 7 | import { getHumanFileSize } from '../../Api/Item'; 8 | 9 | function UploadFileList(props: UploadFileListProps) { 10 | const { files } = props; 11 | const list = Array.from(files).map((f, i) => 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | 20 | return ( 21 |
22 | 23 | {list} 24 | 25 |
26 | ); 27 | } 28 | 29 | interface UploadFileListProps { 30 | files: FileList; 31 | } 32 | 33 | export default UploadFileList; 34 | -------------------------------------------------------------------------------- /src/Components/Dialogs/Dialogs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DialogMenu from './Menu/Menu'; 3 | import DialogSettings from './Settings/Settings'; 4 | import DialogContent from './Content/Content'; 5 | import DialogMedia from './Media/Media'; 6 | import DialogEdit from './Edit/Edit'; 7 | import DialogCreateFolder from './CreateFolder/CreateFolder'; 8 | import DialogCreateFile from './CreateFile/CreateFile'; 9 | import DialogRename from './Rename/Rename'; 10 | import DialogMove from './Move/Move'; 11 | import DialogCopy from './Copy/Copy'; 12 | import DialogUploadFile from './UploadFile/UploadFile'; 13 | 14 | // TODO: Consider moving the visibility logic here 15 | function Dialogs() { 16 | return ( 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | ); 31 | } 32 | 33 | export default Dialogs; 34 | -------------------------------------------------------------------------------- /src/Components/FileList/FileListSublist/FileListSublist.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FileSublist from '../../File/FileSublist/FileSublist'; 3 | import Loader from '../../Loader/Loader'; 4 | import FileListEmptyMessage from '../FileListEmptyMessage'; 5 | import './FileListSublist.css'; 6 | import { FolderItem } from '../../../Api/Item'; 7 | 8 | function FileListSublist(props: OwnProps) { 9 | const { items, isLoading, handleOpenFolder } = props; 10 | 11 | const itemComponents = items.map((item, key) => { 12 | return handleOpenFolder(item)} 16 | handleDoubleClick={() => handleOpenFolder(item)} 17 | key={key} /> 18 | }); 19 | 20 | return
21 | { isLoading ? 22 | : 23 | itemComponents.length ? itemComponents : 24 | } 25 |
26 | } 27 | 28 | interface OwnProps { 29 | items: FolderItem[]; 30 | isLoading: boolean; 31 | handleOpenFolder(folder: FolderItem): void; 32 | } 33 | 34 | export default FileListSublist; -------------------------------------------------------------------------------- /src/Reducers/accountReducer.ts: -------------------------------------------------------------------------------- 1 | import { Action, SET_LOGGED_IN, SET_LOGGED_OUT, SET_HOST, SET_WEB_ID, RESET_HOST, RESET_WEB_ID } from "../Actions/actionTypes"; 2 | import config from "../config"; 3 | 4 | interface AccountState { 5 | loggedIn: boolean; 6 | host: string | null; 7 | webId: string | null; 8 | } 9 | 10 | const initialState: AccountState = { 11 | loggedIn: false, 12 | host: null, 13 | webId: null 14 | }; 15 | 16 | export const account = (state = initialState, action: Action): AccountState => { 17 | switch(action.type) { 18 | case SET_LOGGED_IN: 19 | return { ...state, loggedIn: true }; 20 | case SET_LOGGED_OUT: 21 | return { ...state, loggedIn: false }; 22 | case SET_HOST: 23 | config.setHost(action.value as string); // TODO 24 | return { ...state, host: action.value as string }; 25 | case RESET_HOST: 26 | return { ...state, host: null }; 27 | case SET_WEB_ID: 28 | return { ...state, webId: action.value as string|null }; 29 | case RESET_WEB_ID: 30 | return { ...state, webId: null }; 31 | default: 32 | return state; 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import thunk from 'redux-thunk'; 4 | import { Provider } from 'react-redux'; 5 | import { createStore, applyMiddleware } from 'redux' 6 | import reducer from './Reducers/reducer' 7 | import * as serviceWorker from './serviceWorker'; 8 | import App from './App'; 9 | import './index.css'; 10 | import 'typeface-roboto'; 11 | 12 | const store = createStore(reducer, applyMiddleware(thunk)); 13 | 14 | ReactDOM.render( 15 | 16 | 17 | 18 | , document.getElementById('root')); 19 | 20 | // If you want your app to work offline and load faster, you can change 21 | // unregister() to register() below. Note this comes with some pitfalls. 22 | // Learn more about service workers: http://bit.ly/CRA-PWA 23 | // see issue https://github.com/Otto-AA/solid-filemanager/issues/26 24 | serviceWorker.unregister(); 25 | /* 26 | serviceWorker.register({ 27 | onUpdate: (config) => { 28 | console.group('serviceWorker.onUpdate'); 29 | console.log(config); 30 | console.groupEnd(); 31 | }, 32 | onSuccess: (config) => { 33 | console.group('serviceWorker.onSuccess'); 34 | console.log(config); 35 | console.groupEnd(); 36 | } 37 | }); 38 | */ -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/CopyAction.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import FileCopyIcon from '@material-ui/icons/FileCopy'; 7 | import { openDialog, MyDispatch } from '../../../Actions/Actions'; 8 | import { AppState } from '../../../Reducers/reducer'; 9 | import { DIALOGS } from '../../../Actions/actionTypes'; 10 | 11 | function CopyAction(props: CopyActionProps) { 12 | const { handleClick } = props; 13 | 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | Copy 21 | 22 | 23 | ); 24 | } 25 | 26 | interface CopyActionProps { 27 | handleClick(): void; 28 | } 29 | 30 | const mapStateToProps = (state: AppState) => { 31 | return {}; 32 | }; 33 | 34 | const mapDispatchToProps = (dispatch: MyDispatch) => { 35 | return { 36 | handleClick: () => { 37 | dispatch(openDialog(DIALOGS.COPY)); 38 | } 39 | }; 40 | }; 41 | 42 | export default connect(mapStateToProps, mapDispatchToProps)(CopyAction); 43 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/RenameAction.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import WrapTextIcon from '@material-ui/icons/WrapText'; 7 | import { openDialog, MyDispatch } from '../../../Actions/Actions'; 8 | import { AppState } from '../../../Reducers/reducer'; 9 | import { DIALOGS } from '../../../Actions/actionTypes'; 10 | 11 | function MoveAction(props: MoveActionProps) { 12 | const { handleClick } = props; 13 | 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | Rename 21 | 22 | 23 | ); 24 | } 25 | 26 | interface MoveActionProps { 27 | handleClick(): void; 28 | } 29 | 30 | const mapStateToProps = (state: AppState) => { 31 | return {}; 32 | }; 33 | 34 | const mapDispatchToProps = (dispatch: MyDispatch) => { 35 | return { 36 | handleClick: () => { 37 | dispatch(openDialog(DIALOGS.RENAME)); 38 | } 39 | }; 40 | }; 41 | 42 | export default connect(mapStateToProps, mapDispatchToProps)(MoveAction); 43 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/MoveAction.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import HowToVoteIcon from '@material-ui/icons/HowToVote'; 7 | import { openDialog, MyDispatch } from '../../../Actions/Actions'; 8 | import { AppState } from '../../../Reducers/reducer'; 9 | import { DIALOGS } from '../../../Actions/actionTypes'; 10 | 11 | function MoveAction(props: MoveActionProps) { 12 | const { handleClick } = props; 13 | 14 | return ( 15 | handleClick()}> 16 | 17 | 18 | 19 | 20 | Move 21 | 22 | 23 | ); 24 | } 25 | 26 | interface MoveActionProps { 27 | handleClick(): void; 28 | } 29 | 30 | const mapStateToProps = (state: AppState) => { 31 | return {}; 32 | }; 33 | 34 | const mapDispatchToProps = (dispatch: MyDispatch) => { 35 | return { 36 | handleClick: () => { 37 | dispatch(openDialog(DIALOGS.MOVE)); 38 | } 39 | }; 40 | }; 41 | 42 | export default connect(mapStateToProps, mapDispatchToProps)(MoveAction); 43 | -------------------------------------------------------------------------------- /src/Reducers/dialogsReducer.ts: -------------------------------------------------------------------------------- 1 | import { DIALOGS, Action, OPEN_DIALOG, CLOSE_DIALOG, OPEN_CONTEXT_MENU, CLOSE_CONTEXT_MENU } from "../Actions/actionTypes"; 2 | 3 | // Initialize state with values of DIALOGS as keys and false (closed) as value 4 | const initialVisibleDialogs: Record = Object.values(DIALOGS) 5 | .map((name: DIALOGS) => ({ [name]: false } as Record)) 6 | .reduce((prev, cur) => ({ ...prev, ...cur })) as Record; 7 | 8 | export const visibleDialogs = (state = initialVisibleDialogs, action: Action): typeof initialVisibleDialogs => { 9 | switch (action.type) { 10 | case OPEN_DIALOG: 11 | return { ...state, [action.value]: true }; 12 | case CLOSE_DIALOG: 13 | return { ...state, [action.value]: false }; 14 | default: 15 | return state; 16 | } 17 | }; 18 | 19 | const initialContextMenuState = { 20 | open: false, 21 | x: 0, 22 | y: 0, 23 | }; 24 | 25 | export const contextMenu = (state = initialContextMenuState, action: Action): typeof initialContextMenuState => { 26 | switch (action.type) { 27 | case OPEN_CONTEXT_MENU: 28 | return { 29 | ...state, 30 | open: true, 31 | x: action.value.x, 32 | y: action.value.y 33 | }; 34 | case CLOSE_CONTEXT_MENU: 35 | return { ...state, open: false }; 36 | default: 37 | return state; 38 | } 39 | }; -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/RemoveAction.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import { removeItems, MyDispatch } from '../../../Actions/Actions'; 5 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 6 | import Typography from '@material-ui/core/Typography'; 7 | import DeleteIcon from '@material-ui/icons/Delete'; 8 | import { Item } from '../../../Api/Item'; 9 | import { AppState } from '../../../Reducers/reducer'; 10 | 11 | function RemoveAction(props: RemoveActionProps) { 12 | const { handleClick, selectedItems } = props; 13 | return ( 14 | handleClick(selectedItems)}> 15 | 16 | 17 | 18 | 19 | Remove 20 | 21 | 22 | ); 23 | } 24 | 25 | interface RemoveActionProps { 26 | handleClick(selectedItems: Item[]): void; 27 | selectedItems: Item[]; 28 | } 29 | 30 | const mapStateToProps = (state: AppState) => { 31 | return { 32 | selectedItems: state.items.selected 33 | }; 34 | }; 35 | 36 | const mapDispatchToProps = (dispatch: MyDispatch) => { 37 | return { 38 | handleClick: (selectedItems: Item[]) => { 39 | dispatch(removeItems(selectedItems)); 40 | } 41 | }; 42 | }; 43 | 44 | export default connect(mapStateToProps, mapDispatchToProps)(RemoveAction); 45 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/ZipAction.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import ArchiveIcon from '@material-ui/icons/Archive'; 7 | import { zipAndUpload, MyDispatch } from '../../../Actions/Actions'; 8 | import { Item } from '../../../Api/Item'; 9 | import { AppState } from '../../../Reducers/reducer'; 10 | 11 | function ZipAction(props: ZipActionProps) { 12 | const { handleClick, selectedItems } = props; 13 | 14 | return ( 15 | handleClick(selectedItems)}> 16 | 17 | 18 | 19 | 20 | Zip here 21 | 22 | 23 | ); 24 | } 25 | 26 | interface ZipActionProps { 27 | handleClick(selectedItems: Item[]): void; 28 | selectedItems: Item[]; 29 | } 30 | 31 | const mapStateToProps = (state: AppState) => { 32 | return { 33 | selectedItems: state.items.selected 34 | }; 35 | }; 36 | 37 | const mapDispatchToProps = (dispatch: MyDispatch) => { 38 | return { 39 | handleClick: (selectedItems: Item[]) => { 40 | dispatch(zipAndUpload(selectedItems)); 41 | } 42 | }; 43 | }; 44 | 45 | export default connect(mapStateToProps, mapDispatchToProps)(ZipAction); 46 | -------------------------------------------------------------------------------- /src/Api/contentTypes.ts: -------------------------------------------------------------------------------- 1 | import * as mime from 'mime'; 2 | 3 | export async function guessContentType(fileName: string, blob?: Blob | string, defaultType = 'text/turtle'): Promise { 4 | const extType = mime.getType(fileName) 5 | if (extType) 6 | return extType; 7 | if (blob instanceof Blob) { 8 | const blobType = await guessFromBlob(blob); 9 | if (blobType) 10 | return blobType; 11 | } 12 | return defaultType; 13 | }; 14 | 15 | export function guessFromBlob(blob: Blob): Promise { 16 | const fileReader = new FileReader(); 17 | return new Promise(resolve => { 18 | fileReader.onloadend = e => { 19 | if (e && e.target && e.target.readyState === FileReader.DONE) { 20 | const arr = new Uint8Array(e.target.result as ArrayBuffer); 21 | let header = ''; 22 | for (let i = 0; i < arr.length; i++) 23 | header += arr[i].toString(16); 24 | resolve(magicNumberToMime(header)) 25 | } 26 | }; 27 | fileReader.readAsArrayBuffer(blob.slice(0, 4)); 28 | }); 29 | } 30 | 31 | // TODO: Add more magic numbers 32 | // See https://en.wikipedia.org/wiki/List_of_file_signatures 33 | function magicNumberToMime(num: string): string | undefined { 34 | switch (num) { 35 | case "89504e47": 36 | return "image/png"; 37 | case "47494638": 38 | return "image/gif"; 39 | case "ffd8ffe0": 40 | case "ffd8ffe1": 41 | case "ffd8ffe2": 42 | case "ffd8ffe3": 43 | case "ffd8ffe8": 44 | return "image/jpeg"; 45 | } 46 | return undefined; 47 | } 48 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/EditAction.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import { loadAndEditFile, MyDispatch } from '../../../Actions/Actions'; 5 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 6 | import Typography from '@material-ui/core/Typography'; 7 | import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser'; 8 | import { Item } from '../../../Api/Item'; 9 | import { AppState } from '../../../Reducers/reducer'; 10 | 11 | function OpenAction(props: OpenActionProps) { 12 | const { handleClick, selectedItems } = props; 13 | return ( 14 | handleClick(selectedItems)}> 15 | 16 | 17 | 18 | 19 | Edit 20 | 21 | 22 | ); 23 | } 24 | 25 | interface OpenActionProps { 26 | handleClick(selectedItems: Item[]): void; 27 | selectedItems: Item[]; 28 | } 29 | 30 | const mapStateToProps = (state: AppState) => { 31 | return { 32 | selectedItems: state.items.selected 33 | }; 34 | }; 35 | 36 | const mapDispatchToProps = (dispatch: MyDispatch) => { 37 | return { 38 | handleClick: (selectedItems: Item[]) => { 39 | dispatch(loadAndEditFile(selectedItems[0].name)); 40 | } 41 | }; 42 | }; 43 | 44 | export default connect(mapStateToProps, mapDispatchToProps)(OpenAction); 45 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/ExtractAction.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import UnarchiveIcon from '@material-ui/icons/Unarchive'; 7 | import { extractZipFile, MyDispatch } from '../../../Actions/Actions'; 8 | import { AppState } from '../../../Reducers/reducer'; 9 | import { Item } from '../../../Api/Item'; 10 | 11 | function ExtractAction(props: ExtractActionProps) { 12 | const {handleClick, selectedItems} = props; 13 | 14 | return ( 15 | handleClick(selectedItems)}> 16 | 17 | 18 | 19 | 20 | Extract here 21 | 22 | 23 | ); 24 | } 25 | 26 | interface ExtractActionProps { 27 | handleClick(selectedItems: Item[]): void; 28 | selectedItems: Item[]; 29 | } 30 | 31 | const mapStateToProps = (state: AppState) => { 32 | return { 33 | selectedItems: state.items.selected 34 | }; 35 | }; 36 | 37 | const mapDispatchToProps = (dispatch: MyDispatch) => { 38 | return { 39 | handleClick: (selectedItems: Item[]) => { 40 | dispatch(extractZipFile(selectedItems[0].name)); 41 | } 42 | }; 43 | }; 44 | 45 | export default connect(mapStateToProps, mapDispatchToProps)(ExtractAction); 46 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/OpenInNewTabAction.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import { openInNewTab, MyDispatch } from '../../../Actions/Actions'; 5 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 6 | import Typography from '@material-ui/core/Typography'; 7 | import LinkIcon from '@material-ui/icons/Link'; 8 | import { Item } from '../../../Api/Item'; 9 | import { AppState } from '../../../Reducers/reducer'; 10 | 11 | function OpenInNewTabAction(props: OpenInNewTabActionProps) { 12 | const { handleClick, selectedItems } = props; 13 | return ( 14 | handleClick(selectedItems)}> 15 | 16 | 17 | 18 | 19 | Open in new Tab 20 | 21 | 22 | ); 23 | } 24 | 25 | interface OpenInNewTabActionProps { 26 | handleClick(selectedItems: Item[]): void; 27 | selectedItems: Item[]; 28 | } 29 | 30 | const mapStateToProps = (state: AppState) => { 31 | return { 32 | selectedItems: state.items.selected 33 | }; 34 | }; 35 | 36 | const mapDispatchToProps = (dispatch: MyDispatch) => { 37 | return { 38 | handleClick: (selectedItems: Item[]) => { 39 | dispatch(openInNewTab(selectedItems[0])); 40 | } 41 | }; 42 | }; 43 | 44 | export default connect(mapStateToProps, mapDispatchToProps)(OpenInNewTabAction); 45 | -------------------------------------------------------------------------------- /src/Components/File/FileSublist/FileSublist.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ListItem from '@material-ui/core/ListItem'; 3 | import ListItemAvatar from '@material-ui/core/ListItemAvatar'; 4 | import ListItemText from '@material-ui/core/ListItemText'; 5 | import Avatar from '@material-ui/core/Avatar'; 6 | import FolderIcon from '@material-ui/icons/Folder'; 7 | import FileIcon from '@material-ui/icons/InsertDriveFile'; 8 | import blue from '@material-ui/core/colors/blue'; 9 | import '../File.css'; 10 | import { FileItem, Item } from '../../../Api/Item'; 11 | 12 | // TODO: Check main differences between normal File.tsx component 13 | function FileSublist(props: OwnProps) { 14 | const { item, isSelected, handleClick, handleDoubleClick } = props; 15 | const avatarStyle = { 16 | backgroundColor: isSelected ? blue['A200'] : undefined 17 | }; 18 | return ( 19 |
20 | 21 | 22 | 23 | { (item instanceof FileItem) ? : } 24 | 25 | 26 | 27 | 28 |
29 | ); 30 | } 31 | 32 | interface OwnProps { 33 | item: Item; 34 | isSelected: boolean; 35 | handleClick(): void; 36 | handleDoubleClick(): void; 37 | } 38 | 39 | export default FileSublist; 40 | 41 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/SettingsAction.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import { openDialog, MyDispatch } from '../../../Actions/Actions'; 7 | import { AppState } from '../../../Reducers/reducer'; 8 | import { DIALOGS } from '../../../Actions/actionTypes'; 9 | import { Settings } from '@material-ui/icons'; 10 | 11 | function ChooseLocationAction(props: ChooseLocationActionProps) { 12 | const { handleClick, handleClose } = props; 13 | 14 | const handleCloseAfter = (callback: () => void) => () => { 15 | callback(); 16 | handleClose(); 17 | }; 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | Settings 26 | 27 | 28 | ); 29 | } 30 | 31 | interface ChooseLocationActionProps { 32 | handleClick(): void; 33 | handleClose(): void; 34 | } 35 | 36 | const mapStateToProps = (state: AppState) => { 37 | return {}; 38 | }; 39 | 40 | const mapDispatchToProps = (dispatch: MyDispatch) => { 41 | return { 42 | handleClick: () => { 43 | dispatch(openDialog(DIALOGS.SETTINGS)); 44 | } 45 | }; 46 | }; 47 | 48 | export default connect(mapStateToProps, mapDispatchToProps)(ChooseLocationAction); 49 | -------------------------------------------------------------------------------- /src/Components/FileList/FileList.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import File from '../File/File'; 4 | import FileListEmptyMessage from './FileListEmptyMessage'; 5 | import Loader from '../Loader/Loader'; 6 | import './FileList.css'; 7 | import { Item } from '../../Api/Item'; 8 | import { AppState } from '../../Reducers/reducer'; 9 | 10 | class FileList extends Component { 11 | render() { 12 | const { items, isLoading } = this.props; 13 | const itemComponents = items.map((item, key) => { 14 | return ; 15 | }); 16 | 17 | return
18 | { isLoading ? 19 | : 20 | itemComponents.length ? itemComponents : 21 | } 22 |
23 | } 24 | } 25 | 26 | interface StateProps { 27 | items: Item[]; 28 | isLoading: boolean; 29 | } 30 | interface FileListProps extends StateProps {}; 31 | 32 | const mapStateToProps = (state: AppState): StateProps => { 33 | const items = state.items.inCurFolder 34 | .filter(item => filterMatch(item.getDisplayName(), state.items.filter)); 35 | 36 | return { 37 | items, 38 | isLoading: state.loading, 39 | }; 40 | }; 41 | 42 | 43 | const mapDispatchToProps = () => ({}); 44 | 45 | const filterMatch = (first: string, second: string) => { 46 | return first.toLocaleLowerCase().match(second.toLocaleLowerCase()); 47 | } 48 | 49 | export default connect(mapStateToProps, mapDispatchToProps)(FileList); 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/UploadFileAction.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import CloudUploadIcon from '@material-ui/icons/CloudUpload'; 7 | import { openDialog, MyDispatch } from '../../../Actions/Actions'; 8 | import { AppState } from '../../../Reducers/reducer'; 9 | import { DIALOGS } from '../../../Actions/actionTypes'; 10 | 11 | function UploadFileAction(props: UploadFileActionProps) { 12 | const { handleClick, handleClose } = props; 13 | 14 | const handleCloseAfter = (callback: () => void) => () => { 15 | callback(); 16 | handleClose(); 17 | }; 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | Upload files 26 | 27 | 28 | ); 29 | } 30 | 31 | interface UploadFileActionProps { 32 | handleClick(): void; 33 | handleClose(): void; 34 | } 35 | 36 | const mapStateToProps = (state: AppState) => { 37 | return {}; 38 | }; 39 | 40 | const mapDispatchToProps = (dispatch: MyDispatch) => { 41 | return { 42 | handleClick: () => { 43 | dispatch(openDialog(DIALOGS.UPLOAD_FILE)); 44 | } 45 | }; 46 | }; 47 | 48 | export default connect(mapStateToProps, mapDispatchToProps)(UploadFileAction); 49 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/CreateFileAction.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import InsertDriveFileIcon from '@material-ui/icons/InsertDriveFile'; 7 | import { openDialog, MyDispatch } from '../../../Actions/Actions'; 8 | import { AppState } from '../../../Reducers/reducer'; 9 | import { DIALOGS } from '../../../Actions/actionTypes'; 10 | 11 | function CreateFileAction(props: CreateFileActionProps) { 12 | const {handleClick, handleClose} = props; 13 | 14 | const handleCloseAfter = (callback: () => void) => () => { 15 | callback(); 16 | handleClose(); 17 | }; 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | Create file 26 | 27 | 28 | ); 29 | } 30 | 31 | interface CreateFileActionProps { 32 | handleClick(): void; 33 | handleClose(): void; 34 | } 35 | 36 | const mapStateToProps = (state: AppState) => { 37 | return {}; 38 | }; 39 | 40 | const mapDispatchToProps = (dispatch: MyDispatch) => { 41 | return { 42 | handleClick: () => { 43 | dispatch(openDialog(DIALOGS.CREATE_FILE)); 44 | } 45 | }; 46 | }; 47 | 48 | export default connect(mapStateToProps, mapDispatchToProps)(CreateFileAction); 49 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/CreateFolderAction.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import CreateNewFolderIcon from '@material-ui/icons/CreateNewFolder'; 7 | import { openDialog, MyDispatch } from '../../../Actions/Actions'; 8 | import { AppState } from '../../../Reducers/reducer'; 9 | import { DIALOGS } from '../../../Actions/actionTypes'; 10 | 11 | function CreateFolderAction(props: CreateFolderActionProps) { 12 | const {handleClick, handleClose} = props; 13 | 14 | const handleCloseAfter = (callback: () => void) => () => { 15 | callback(); 16 | handleClose(); 17 | }; 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | Create folder 26 | 27 | 28 | ); 29 | } 30 | 31 | interface CreateFolderActionProps { 32 | handleClick(): void; 33 | handleClose(): void; 34 | } 35 | 36 | const mapStateToProps = (state: AppState) => { 37 | return {}; 38 | }; 39 | 40 | const mapDispatchToProps = (dispatch: MyDispatch) => { 41 | return { 42 | handleClick: () => { 43 | dispatch(openDialog(DIALOGS.CREATE_FOLDER)); 44 | } 45 | }; 46 | }; 47 | 48 | export default connect(mapStateToProps, mapDispatchToProps)(CreateFolderAction); 49 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/ChooseLocationAction.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import FolderSharedIcon from '@material-ui/icons/FolderSharedOutlined'; 7 | import { openDialog, MyDispatch } from '../../../Actions/Actions'; 8 | import { AppState } from '../../../Reducers/reducer'; 9 | import { DIALOGS } from '../../../Actions/actionTypes'; 10 | 11 | function ChooseLocationAction(props: ChooseLocationActionProps) { 12 | const { handleClick, handleClose } = props; 13 | 14 | const handleCloseAfter = (callback: () => void) => () => { 15 | callback(); 16 | handleClose(); 17 | }; 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | Choose root location 26 | 27 | 28 | ); 29 | } 30 | 31 | interface ChooseLocationActionProps { 32 | handleClick(): void; 33 | handleClose(): void; 34 | } 35 | 36 | const mapStateToProps = (state: AppState) => { 37 | return {}; 38 | }; 39 | 40 | const mapDispatchToProps = (dispatch: MyDispatch) => { 41 | return { 42 | handleClick: () => { 43 | dispatch(openDialog(DIALOGS.CHOOSE_LOCATION)); 44 | } 45 | }; 46 | }; 47 | 48 | export default connect(mapStateToProps, mapDispatchToProps)(ChooseLocationAction); 49 | -------------------------------------------------------------------------------- /src/Reducers/itemsReducer.ts: -------------------------------------------------------------------------------- 1 | import { Item } from "../Api/Item"; 2 | import { Action, SET_ITEMS, SELECT_ITEMS, DESELECT_ITEM, FILTER_ITEMS, RESET_FILTER, TOGGLE_SELECTED_ITEM } from "../Actions/actionTypes"; 3 | 4 | interface ItemsState { 5 | inCurFolder: Item[]; 6 | filter: string; 7 | selected: Item[]; 8 | } 9 | 10 | const initialItemsState: ItemsState = { 11 | inCurFolder: [], 12 | filter: '', 13 | selected: [], 14 | }; 15 | 16 | export const items = (state = initialItemsState, action: Action): ItemsState => { 17 | switch(action.type) { 18 | case SET_ITEMS: 19 | return { ...state, inCurFolder: action.value as Item[] }; 20 | case SELECT_ITEMS: 21 | return { ...state, selected: action.value as Item[] }; 22 | case DESELECT_ITEM: 23 | return { ...state, selected: removeItem(state.selected, action.value as Item) }; 24 | case TOGGLE_SELECTED_ITEM: 25 | return { 26 | ...state, 27 | selected: state.selected.includes(action.value) ? 28 | removeItem(state.selected, action.value as Item) 29 | : addItem(state.selected, action.value as Item) 30 | }; 31 | case FILTER_ITEMS: 32 | return { ...state, filter: action.value as string }; 33 | case RESET_FILTER: 34 | return { ...state, filter: '' }; 35 | default: 36 | return state; 37 | } 38 | }; 39 | 40 | const removeItem = (items: Item[], item: Item) => { 41 | return items.filter(selectedItem => !selectedItem.equals(item)); 42 | } 43 | 44 | const addItem = (items: Item[], item: Item) => { 45 | return [...items, item]; 46 | } -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/DownloadAction.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import { downloadItems, MyDispatch } from '../../../Actions/Actions'; 5 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 6 | import Typography from '@material-ui/core/Typography'; 7 | import CloudDownloadIcon from '@material-ui/icons/CloudDownload'; 8 | import { FileItem, Item } from '../../../Api/Item'; 9 | import { AppState } from '../../../Reducers/reducer'; 10 | 11 | function DownloadAction(props: DownloadActionProps) { 12 | const { handleClick, selectedItems } = props; 13 | return ( 14 | handleClick(selectedItems)}> 15 | 16 | 17 | 18 | 19 | {(selectedItems.length === 1 && selectedItems[0] instanceof FileItem) ? 20 | 'Download' 21 | : 'Download Zip' 22 | } 23 | 24 | 25 | ); 26 | } 27 | 28 | interface DownloadActionProps { 29 | handleClick(selectedItems: Item[]): void; 30 | selectedItems: Item[]; 31 | } 32 | 33 | const mapStateToProps = (state: AppState) => { 34 | return { 35 | selectedItems: state.items.selected 36 | }; 37 | }; 38 | 39 | const mapDispatchToProps = (dispatch: MyDispatch) => { 40 | return { 41 | handleClick: (selectedItems: Item[]) => { 42 | dispatch(downloadItems(selectedItems)); 43 | } 44 | }; 45 | }; 46 | 47 | export default connect(mapStateToProps, mapDispatchToProps)(DownloadAction); 48 | -------------------------------------------------------------------------------- /src/Components/Dialogs/Move/Move.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { moveItems, closeDialog, MyDispatch } from '../../../Actions/Actions'; 4 | import { DialogStateProps } from '../dialogTypes'; 5 | import { Item } from '../../../Api/Item'; 6 | import { AppState } from '../../../Reducers/reducer'; 7 | import { DIALOGS } from '../../../Actions/actionTypes'; 8 | 9 | import ChooseLocation from '../ChooseLocation/ChooseLocation'; 10 | 11 | function MoveDialog(props: MoveProps) { 12 | const { initialHost, initialPath, selectedItems, open, handleClose, move } = props; 13 | 14 | return move(selectedItems, location)} 21 | /> 22 | } 23 | 24 | 25 | interface StateProps extends DialogStateProps { 26 | initialHost: string; 27 | initialPath: string[]; 28 | selectedItems: Item[]; 29 | } 30 | interface DispatchProps { 31 | handleClose(): void; 32 | move(selectedItems: Item[], { host, path }: { host: string, path: string[] }): void; 33 | } 34 | interface MoveProps extends StateProps, DispatchProps {} 35 | 36 | 37 | 38 | const mapStateToProps = (state: AppState): StateProps => { 39 | return { 40 | open: state.visibleDialogs.MOVE, 41 | initialHost: state.account.host || '', 42 | initialPath: state.path, 43 | selectedItems: state.items.selected, 44 | }; 45 | }; 46 | 47 | const mapDispatchToProps = (dispatch: MyDispatch): DispatchProps => { 48 | return { 49 | handleClose: () => { 50 | dispatch(closeDialog(DIALOGS.MOVE)); 51 | }, 52 | move: (selectedItems, targetLocation) => { 53 | dispatch(moveItems(selectedItems, targetLocation)); 54 | }, 55 | }; 56 | }; 57 | 58 | export default connect(mapStateToProps, mapDispatchToProps)(MoveDialog); -------------------------------------------------------------------------------- /src/Components/Dialogs/Copy/Copy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { copyItems, closeDialog, MyDispatch } from '../../../Actions/Actions'; 4 | import { DialogStateProps } from '../dialogTypes'; 5 | import { Item } from '../../../Api/Item'; 6 | import { AppState } from '../../../Reducers/reducer'; 7 | import { DIALOGS } from '../../../Actions/actionTypes'; 8 | 9 | import ChooseLocation from '../ChooseLocation/ChooseLocation'; 10 | 11 | function CopyDialog(props: CopyProps) { 12 | const { initialHost, initialPath, selectedItems, open, handleClose, copy } = props; 13 | 14 | return copy(selectedItems, location)} 21 | /> 22 | } 23 | 24 | 25 | interface StateProps extends DialogStateProps { 26 | initialHost: string; 27 | initialPath: string[]; 28 | selectedItems: Item[]; 29 | } 30 | interface DispatchProps { 31 | handleClose(): void; 32 | copy(selectedItems: Item[], { host, path }: { host: string, path: string[] }): void; 33 | } 34 | interface CopyProps extends StateProps, DispatchProps {} 35 | 36 | 37 | 38 | const mapStateToProps = (state: AppState): StateProps => { 39 | return { 40 | open: state.visibleDialogs.COPY, 41 | initialHost: state.account.host || '', 42 | initialPath: state.path, 43 | selectedItems: state.items.selected, 44 | }; 45 | }; 46 | 47 | const mapDispatchToProps = (dispatch: MyDispatch): DispatchProps => { 48 | return { 49 | handleClose: () => { 50 | dispatch(closeDialog(DIALOGS.COPY)); 51 | }, 52 | copy: (selectedItems, targetLocation) => { 53 | dispatch(copyItems(selectedItems, targetLocation)); 54 | }, 55 | }; 56 | }; 57 | 58 | export default connect(mapStateToProps, mapDispatchToProps)(CopyDialog); 59 | -------------------------------------------------------------------------------- /src/Actions/actionTypes.ts: -------------------------------------------------------------------------------- 1 | export interface Action { 2 | type: string; 3 | value: V; 4 | }; 5 | 6 | export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE'; 7 | export const ENTER_FOLDER = 'ENTER_FOLDER'; 8 | export const MOVE_FOLDER_UPWARDS = 'MOVE_FOLDER_UPWARDS'; 9 | export const SET_PATH = 'SET_PATH'; 10 | 11 | export const SET_LOGGED_IN = 'SET_LOGGED_IN'; 12 | export const SET_LOGGED_OUT = 'SET_LOGGED_OUT'; 13 | export const RESET_HOST = 'RESET_HOST'; 14 | export const SET_HOST = 'SET_HOST'; // TODO: Consider renaming to BASE_URL 15 | export const RESET_WEB_ID = 'RESET_WEB_ID'; 16 | export const SET_WEB_ID = 'SET_WEB_ID'; 17 | 18 | export const TOGGLE_WITH_ACL = 'TOGGLE_WITH_ACL'; 19 | export const TOGGLE_WITH_META = 'TOGGLE_WITH_META'; 20 | 21 | export const SET_ITEMS = 'SET_ITEMS'; 22 | 23 | export const SELECT_ITEMS = 'SELECT_ITEMS'; 24 | export const TOGGLE_SELECTED_ITEM = 'TOGGLE_SELECTED_ITEM'; 25 | export const DESELECT_ITEM = 'DESELECT_ITEM'; 26 | 27 | export const FILTER_ITEMS = 'FILTER_ITEMS'; 28 | export const RESET_FILTER = 'REMOVE_FILTER'; 29 | 30 | export const DISPLAY_LOADING = 'DISPLAY_LOADING'; 31 | export const STOP_LOADING = 'STOP_LOADING'; 32 | 33 | export const RESET_LOADED_BLOB = 'RESET_LOADED_BLOB'; 34 | export const SET_LOADED_BLOB = 'SET_LOADED_BLOB'; 35 | 36 | export const SET_UPLOAD_FILE_PROGRESS = 'SET_UPLOAD_FILE_PROGRESS'; 37 | export const SET_UPLOAD_FILE_LIST = 'SET_UPLOAD_FILE_LIST'; 38 | 39 | export const OPEN_CONTEXT_MENU = 'OPEN_CONTEXT_MENU'; 40 | export const CLOSE_CONTEXT_MENU = 'CLOSE_CONTEXT_MENU'; 41 | 42 | export const OPEN_DIALOG = 'OPEN_DIALOG'; 43 | export const CLOSE_DIALOG = 'CLOSE_DIALOG'; 44 | 45 | export enum DIALOGS { 46 | CHOOSE_LOCATION = 'CHOOSE_LOCATION', 47 | CREATE_FOLDER = 'CREATE_FOLDER', 48 | CREATE_FILE = 'CREATE_FILE', 49 | UPLOAD_FILE = 'UPLOAD_FILE', 50 | RENAME = 'RENAME', 51 | MOVE = 'MOVE', 52 | COPY = 'COPY', 53 | CONTENT = 'CONTENT', 54 | MEDIA = 'MEDIA', 55 | EDIT = 'EDIT', 56 | CONTEXT_MENU = 'CONTEXT_MENU', 57 | SETTINGS = 'SETTINGS', 58 | }; -------------------------------------------------------------------------------- /src/Components/FileUploader/FileUploader.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import UploadFileList from './UploadFileList'; 4 | 5 | class FileUploader extends Component { 6 | inputRef: React.RefObject = createRef(); 7 | 8 | handleReset(event: React.MouseEvent | React.TouchEvent): void { 9 | const inputElement = this.inputRef.current; 10 | if (inputElement) { 11 | inputElement.value = ''; 12 | this.props.handleReset(event); 13 | } 14 | } 15 | 16 | render() { 17 | const { fileList, handleSelectedFiles } = this.props; 18 | const styles = { 19 | inputfile: { 20 | // TODO: Change this to display none as soon, as the label button works 21 | // display: 'none' 22 | }, inputreset: { 23 | display: (fileList && fileList.length) ? 'inline-flex' : 'none' 24 | } 25 | } 26 | 27 | return ( 28 |
29 | 35 | 36 | 39 | 40 | { fileList && } 41 |
42 | ); 43 | } 44 | } 45 | 46 | interface FileUploadProps { 47 | fileList: FileList|null; 48 | handleReset(event: React.MouseEvent | React.TouchEvent): void; 49 | handleSelectedFiles(event: React.ChangeEvent): void; 50 | } 51 | 52 | export default FileUploader; 53 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenuActions/OpenAction.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | import { connect } from 'react-redux'; 4 | import { loadAndDisplayFile, displaySelectedMediaFile, loadAndEditFile, enterFolderByItem, MyDispatch } from '../../../Actions/Actions'; 5 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 6 | import Typography from '@material-ui/core/Typography'; 7 | import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser'; 8 | import { FileItem, FolderItem, Item } from '../../../Api/Item'; 9 | import { AppState } from '../../../Reducers/reducer'; 10 | 11 | function OpenAction(props: OpenActionProps) { 12 | const { handleClick, selectedItems } = props; 13 | return ( 14 | handleClick(selectedItems)}> 15 | 16 | 17 | 18 | 19 | Open 20 | 21 | 22 | ); 23 | } 24 | 25 | interface OpenActionProps { 26 | handleClick(selectedItems: Item[]): void; 27 | selectedItems: Item[]; 28 | } 29 | 30 | const mapStateToProps = (state: AppState) => { 31 | return { 32 | selectedItems: state.items.selected 33 | }; 34 | }; 35 | 36 | const mapDispatchToProps = (dispatch: MyDispatch) => { 37 | return { 38 | handleClick: (selectedItems: Item[]) => { 39 | const item = selectedItems[0]; 40 | 41 | if (item instanceof FolderItem) 42 | dispatch(enterFolderByItem(item)); 43 | else if (item instanceof FileItem) { 44 | if (item.isEditable()) 45 | dispatch(loadAndEditFile(item.name)); 46 | else if (item.isImage()) 47 | dispatch(loadAndDisplayFile(item.name)); 48 | else if (item.isMedia()) 49 | dispatch(displaySelectedMediaFile()); 50 | } 51 | } 52 | }; 53 | }; 54 | 55 | export default connect(mapStateToProps, mapDispatchToProps)(OpenAction); 56 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | Solid Filemanager 25 | 44 | 45 | 46 |
47 |
48 |
49 | 50 | 51 | -------------------------------------------------------------------------------- /src/Components/Navbar/ThreeDotsMenu.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Menu from '@material-ui/core/Menu'; 3 | import IconButton from '@material-ui/core/IconButton'; 4 | import MoreVertIcon from '@material-ui/icons/MoreVert'; 5 | import { connect } from 'react-redux'; 6 | import CreateFolderAction from '../ContextMenu/ContextMenuActions/CreateFolderAction'; 7 | import CreateFileAction from '../ContextMenu/ContextMenuActions/CreateFileAction'; 8 | import UploadFileAction from '../ContextMenu/ContextMenuActions/UploadFileAction'; 9 | import ChooseLocationAction from '../ContextMenu/ContextMenuActions/ChooseLocationAction'; 10 | import SettingsAction from '../ContextMenu/ContextMenuActions/SettingsAction'; 11 | 12 | class ThreeDotsMenu extends React.Component { 13 | state = { 14 | anchorEl: null as HTMLElement|null, 15 | }; 16 | 17 | handleClick = (event: React.MouseEvent) => { 18 | this.setState({ anchorEl: event.currentTarget }); 19 | }; 20 | 21 | handleClose = () => { 22 | this.setState({ anchorEl: null }); 23 | }; 24 | 25 | render() { 26 | const { anchorEl } = this.state; 27 | 28 | return ( 29 |
30 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | ); 48 | } 49 | } 50 | 51 | 52 | const mapStateToProps = () => ({}); 53 | 54 | const mapDispatchToProps = () => ({}); 55 | 56 | export default connect(mapStateToProps, mapDispatchToProps)(ThreeDotsMenu); 57 | -------------------------------------------------------------------------------- /src/Components/Notification/DynamicSnackbar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withStyles, Theme, createStyles, WithStyles } from '@material-ui/core/styles'; 3 | import Snackbar from '@material-ui/core/Snackbar'; 4 | import IconButton from '@material-ui/core/IconButton'; 5 | import CloseIcon from '@material-ui/icons/Close'; 6 | import { connect } from 'react-redux'; 7 | import { MyDispatch, resetErrorMessage } from '../../Actions/Actions'; 8 | import { AppState } from '../../Reducers/reducer'; 9 | 10 | const styles = (theme: Theme) => createStyles({ 11 | close: { 12 | padding: theme.spacing() / 2, 13 | }, 14 | }); 15 | 16 | class DynamicSnackbar extends React.Component { 17 | render() { 18 | const { classes, errorMsg, handleClose, open, notificationDuration } = this.props; 19 | return ( 20 |
21 | {errorMsg}} 33 | action={[ 34 | 35 | 36 | , 37 | ]} 38 | /> 39 |
40 | ); 41 | } 42 | } 43 | 44 | interface StateProps { 45 | open: boolean; 46 | errorMsg: string; 47 | notificationDuration: number; 48 | } 49 | interface DispatchProps { 50 | handleClose(): void; 51 | } 52 | interface DynamicSnackbarProps extends StateProps, DispatchProps, WithStyles {} 53 | 54 | const mapStateToProps = (state: AppState): StateProps => { 55 | return { 56 | open: !!state.errorMessage, 57 | errorMsg: state.errorMessage, 58 | notificationDuration: 60000 59 | }; 60 | }; 61 | 62 | const mapDispatchToProps = (dispatch: MyDispatch): DispatchProps => { 63 | return { 64 | handleClose: () => { 65 | dispatch(resetErrorMessage()); 66 | } 67 | }; 68 | }; 69 | 70 | export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(DynamicSnackbar)); 71 | 72 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import FileList from './Components/FileList/FileList'; 3 | import Navbar from './Components/Navbar/Navbar'; 4 | import ContextMenu from './Components/ContextMenu/ContextMenu'; 5 | import Dialogs from './Components/Dialogs/Dialogs'; 6 | 7 | import { MuiThemeProvider as MaterialUI } from '@material-ui/core/styles'; 8 | import { createTheme } from '@material-ui/core' 9 | import blue from '@material-ui/core/colors/blue'; 10 | import { connect } from 'react-redux'; 11 | import { initApp, MyDispatch, closeContextMenu } from './Actions/Actions'; 12 | import DynamicSnackbar from './Components/Notification/DynamicSnackbar'; 13 | import HistoryHandler from './Components/HistoryHandler/HistoryHandler'; 14 | 15 | const theme = createTheme({ 16 | palette: { 17 | primary: blue, 18 | }, 19 | }); 20 | 21 | class App extends Component { 22 | 23 | componentDidMount() { 24 | this.props.init(); 25 | }; 26 | 27 | render() { 28 | return ( 29 |
30 | 31 |
32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 | 40 |
41 | ); 42 | } 43 | } 44 | 45 | interface DispatchProps { 46 | init(): void; 47 | handleHideContextMenu(event: React.MouseEvent): void; 48 | } 49 | 50 | interface AppProps extends DispatchProps {} 51 | 52 | const mapStateToProps = () => ({}); 53 | 54 | const mapDispatchToProps = (dispatch: MyDispatch): DispatchProps => { 55 | return { 56 | init: () => { 57 | dispatch(initApp()); 58 | }, 59 | 60 | handleHideContextMenu: (event) => { 61 | const element = event.target as HTMLElement; 62 | if (!(element.tagName === 'INPUT' || /label/i.test(element.className))) { 63 | event.preventDefault(); 64 | } 65 | dispatch(closeContextMenu()); 66 | } 67 | }; 68 | }; 69 | 70 | export default connect(mapStateToProps, mapDispatchToProps)(App); 71 | -------------------------------------------------------------------------------- /src/Components/Breadcrumb/BreadcrumbText.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { withStyles, createStyles, Theme, WithStyles } from '@material-ui/core/styles'; 4 | import KeyboardArrowLeftIcon from '@material-ui/icons/KeyboardArrowLeft'; 5 | import Button from '@material-ui/core/Button'; 6 | import './BreadcrumbText.css'; 7 | 8 | const styles = (theme: Theme) => createStyles({ 9 | lastPath: { 10 | display: 'block', 11 | [theme.breakpoints.up('sm')]: { 12 | display: 'none' 13 | } 14 | }, 15 | paths: { 16 | display: 'none', 17 | [theme.breakpoints.up('sm')]: { 18 | display: 'block', 19 | } 20 | } 21 | }); 22 | 23 | class BreadcrumbText extends Component { 24 | 25 | render() { 26 | const { classes, handleClickPath, path, rootTitle, handleGoBack, canGoBack } = this.props; 27 | 28 | const separator = >; 29 | const rootPath = handleClickPath(-1)} data-index={0}> 30 | { rootTitle } { path.length ? separator : '' } 31 | ; 32 | const lastPath = [...path].pop() || rootTitle; 33 | 34 | const directories = path.map((dir, index) => { 35 | return handleClickPath(index)}> 36 | {dir} { path.length -1 !== index ? separator : '' }  37 | 38 | }); 39 | 40 | return ( 41 |
42 |
43 | 46 | {lastPath} 47 |
48 |
{rootPath} {directories}
49 |
50 | ); 51 | } 52 | } 53 | 54 | interface BreadcrumbTextProps extends WithStyles { 55 | handleClickPath(index: number): void; 56 | handleGoBack(): void; 57 | canGoBack: boolean; 58 | path: string[]; 59 | rootTitle: string; 60 | 61 | } 62 | 63 | const mapDispatchToProps = () => ({}); 64 | 65 | const mapStateToProps = () => ({}); 66 | 67 | export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(BreadcrumbText)); 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 2 | 3 | **NOTE**: This project has no active maintainer. It may or may not be working, and from time to time stuff will probably get fixed. 4 | 5 | # Solid Filemanager 6 | 7 | This is a file manager for solid pods, based on [this awesome react app](https://github.com/joni2back/react-filemanager/). 8 | 9 | The app is available here: https://otto-aa.github.io/solid-filemanager/ (see below for more options). To use it you need to have a [solid pod](https://solid.inrupt.com/get-a-solid-pod). If the pod is hosted with a newer server version (5.0.0+) you will need to give this app explicit permission to read/write data to your pod (go to .../profile/card#me -> click on "A" in the top -> Add "https://otto-aa.github.io" with read/write access). 10 | 11 | ## Features 12 | 13 | - Navigation through folders 14 | - Upload files 15 | - Copy, remove, move and rename file and folders 16 | - Edit text files (txt, html, ...) 17 | - View media files (video, audio and image files) 18 | - Zip actions (archive and extract) 19 | - Download files 20 | - Open files in a new tab 21 | 22 | ![Screenshot of the file manager](./images/Screenshot.png "Demo Screenshot") 23 | 24 | ## Hosting the app 25 | It's easy to install your own version of this app. This would have the benefit, that it is independent from this repository, but the disadvantage of getting no updates. 26 | 27 | 1. Go to the [gh-pages](https://github.com/Otto-AA/solid-filemanager/tree/gh-pages) branch 28 | 2. Click on the green download button and save the zip file 29 | 3. Upload the zip to your pod/server (e.g. with this file manager) 30 | 4. Extract the zip file 31 | 5. Open the app in a new tab 32 | 33 | ## Developing 34 | If you want to modify this app, first make sure you've installed git, node and npm. Then enter following commands: 35 | 36 | ```shell 37 | git clone https://github.com/otto-aa/solid-filemanager/ # Downloads the source 38 | cd solid-filemenager # Enter the directory 39 | npm install # Install dependencies 40 | npm start # Start the development app 41 | # Make changes to the source code now 42 | ``` 43 | 44 | With `npm build` you can create a static build, but keep in mind that logging into your solid pod requires the app to run on a domain or localhost (and not `file:///C:/.../index.html`). So either deploy it (to your pod, etc.) or run it via localhost (e.g. with npm serve). 45 | 46 | ## Contribute 47 | 48 | Feel free to make contributions by adding features, filing issues, fixing bugs, making suggestions, etc. 49 | -------------------------------------------------------------------------------- /src/Components/Dialogs/Content/Content.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import Dialog from '@material-ui/core/Dialog'; 4 | import DialogActions from '@material-ui/core/DialogActions'; 5 | import DialogContent from '@material-ui/core/DialogContent'; 6 | import DialogTitle from '@material-ui/core/DialogTitle'; 7 | import { connect } from 'react-redux'; 8 | import { closeDialog, MyDispatch } from '../../../Actions/Actions'; 9 | import { DialogStateProps, DialogDispatchProps } from '../dialogTypes'; 10 | import { AppState } from '../../../Reducers/reducer'; 11 | import { DIALOGS } from '../../../Actions/actionTypes'; 12 | 13 | class FormDialog extends Component { 14 | 15 | state = { 16 | lastBlobUrl: null, 17 | content: '...', 18 | loading: false 19 | }; 20 | 21 | componentDidUpdate() { 22 | if (this.props.blobUrl !== this.state.lastBlobUrl) { 23 | this.setState({ 24 | lastBlobUrl: this.props.blobUrl 25 | }); 26 | this.setState({ 27 | loading: true 28 | }); 29 | } 30 | } 31 | 32 | render() { 33 | const { handleClose, open } = this.props; 34 | return ( 35 |
36 | 37 | Viewing file 38 | 39 | 40 | 41 | 42 | 45 | 46 | 47 |
48 | ); 49 | } 50 | } 51 | 52 | interface StateProps extends DialogStateProps { 53 | blobUrl: string | undefined; 54 | } 55 | interface DispatchProps extends DialogDispatchProps {} 56 | interface ContentProps extends StateProps, DispatchProps {} 57 | 58 | const mapStateToProps = (state: AppState): StateProps => { 59 | return { 60 | open: state.visibleDialogs.CONTENT, 61 | blobUrl: state.blob || undefined 62 | }; 63 | }; 64 | 65 | const mapDispatchToProps = (dispatch: MyDispatch): DialogDispatchProps => { 66 | return { 67 | handleClose: () => { 68 | dispatch(closeDialog(DIALOGS.CONTENT)); 69 | } 70 | }; 71 | }; 72 | 73 | export default connect(mapStateToProps, mapDispatchToProps)(FormDialog); 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solid-filemanager", 3 | "version": "1.2.1", 4 | "private": false, 5 | "scripts": { 6 | "predeploy": "npm run build", 7 | "deploy": "gh-pages -d build", 8 | "start": "react-scripts start", 9 | "build": "react-scripts build", 10 | "test": "react-scripts test", 11 | "eject": "react-scripts eject", 12 | "test:e2e": "npm run start-wait && cypress run", 13 | "precss": "node delete_css_storage.js", 14 | "css": "community-solid-server -p 8080 -c @css:config/file-no-setup.json -f ./community-solid-server", 15 | "start-wait": "npm run start-wait:frontend && npm run start-wait:css", 16 | "start-wait:frontend": "npm run start & wait-on http://localhost:3000", 17 | "start-wait:css": "npm run css & wait-on http://localhost:8080" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/otto-aa/solid-filemanager.git" 22 | }, 23 | "keywords": [ 24 | "filemanager", 25 | "solid" 26 | ], 27 | "author": "A_A", 28 | "bugs": { 29 | "url": "https://github.com/otto-aa/solid-filemanager/issues" 30 | }, 31 | "homepage": "https://otto-aa.github.io/solid-filemanager/", 32 | "dependencies": { 33 | "@inrupt/solid-client-authn-browser": "^2.3.0", 34 | "@material-ui/core": "^4.12.4", 35 | "@material-ui/icons": "^4.11.3", 36 | "@types/classnames": "^2.2.10", 37 | "@types/history": "^4.7.5", 38 | "@types/jest": "^24.9.1", 39 | "@types/jszip": "^3.1.7", 40 | "@types/material-ui": "^0.21.12", 41 | "@types/mime": "^2.0.1", 42 | "@types/node": "^11.15.12", 43 | "@types/rdflib": "^0.17.1", 44 | "@types/react": "^16.9.34", 45 | "@types/react-dom": "^16.9.7", 46 | "@types/react-plyr": "^2.1.0", 47 | "@types/react-redux": "^7.1.7", 48 | "gh-pages": "^3.2.3", 49 | "history": "^4.10.1", 50 | "jszip": "^3.4.0", 51 | "mime": "^2.4.4", 52 | "plyr": "^3.5.10", 53 | "rdflib": "^2.2.19", 54 | "react": "^16.13.1", 55 | "react-dom": "^16.13.1", 56 | "react-plyr": "^2.2.0", 57 | "react-redux": "^6.0.1", 58 | "react-scripts": "^5.0.0", 59 | "redux": "^4.0.5", 60 | "redux-thunk": "~2.3.0", 61 | "solid-file-client": "^2.1.3", 62 | "typeface-roboto": "0.0.54", 63 | "typescript": "^3.8.3" 64 | }, 65 | "eslintConfig": { 66 | "extends": "react-app" 67 | }, 68 | "browserslist": [ 69 | ">0.2%", 70 | "not dead", 71 | "not ie <= 11", 72 | "not op_mini all" 73 | ], 74 | "devDependencies": { 75 | "@inrupt/solid-client-authn-core": "^1.12.1", 76 | "@solid/community-server": "^4.0.0", 77 | "cypress": "^9.6.0", 78 | "uuid": "^8.3.2", 79 | "wait-on": "^6.0.1" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Api/Item.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Class for an arbitrary item from a solid pod 3 | */ 4 | export class Item { 5 | _name: string; 6 | _path: string[]; 7 | _url: string; 8 | _size?: string 9 | 10 | constructor(url: string, size?: string) { 11 | const path = getPathFromUrl(url); 12 | 13 | this._name = path.pop() || ''; 14 | this._path = path; 15 | this._url = url; 16 | this._size = size; 17 | } 18 | 19 | // Make properties readonly 20 | get name() { return this._name; } 21 | get path() { return this._path; } 22 | get url() { return this._url; } 23 | get size() { return this._size; } 24 | 25 | equals(item: Item) { 26 | return this.name === item.name 27 | && this.path.length === item.path.length 28 | && this.path.every((val, index) => val === item.path[index]); 29 | } 30 | 31 | getDisplayName() { 32 | return decodeURI(this.name); 33 | } 34 | 35 | getDisplaySize() { 36 | return this._size ? getHumanFileSize(this._size) : 'Unknown size'; 37 | } 38 | } 39 | 40 | export class FileItem extends Item { 41 | isImage() { 42 | return patterns.image.test(this.name); 43 | } 44 | 45 | isMedia() { 46 | return patterns.media.test(this.name); 47 | } 48 | 49 | isEditable() { 50 | return patterns.editable.test(this.name); 51 | } 52 | 53 | isExtractable() { 54 | return patterns.extractable.test(this.name); 55 | } 56 | 57 | isVideo() { 58 | return patterns.video.test(this.name); 59 | } 60 | } 61 | 62 | export class FolderItem extends Item { } 63 | 64 | 65 | // regex patterns for testing if a file is of a specific type 66 | const patterns = { 67 | editable: /\.(txt|diff?|patch|svg|asc|cnf|cfg|conf|html?|cfm|cgi|aspx?|ini|pl|py|md|css|cs|jsx?|jsp|log|htaccess|htpasswd|gitignore|gitattributes|env|json|atom|eml|rss|markdown|sql|xml|xslt?|sh|rb|as|bat|cmd|cob|for|ftn|frm|frx|inc|lisp|scm|coffee|php[3-6]?|java|c|cbl|go|h|scala|vb|tmpl|lock|go|yml|yaml|tsv|lst|ttl)$/i, 68 | image: /\.(jpe?g|gif|bmp|png|svg|tiff?)$/i, 69 | media: /\.(mp3|ogg|wav|mp4|webm)$/i, 70 | video: /\.(mp4|webm|ogg)$/i, 71 | extractable: /\.(zip)$/i 72 | }; 73 | 74 | /** 75 | * Calculate file size by bytes in human readable format 76 | */ 77 | export const getHumanFileSize = (byteString: string|number): string => { 78 | const bytes = typeof byteString === 'string' ? 79 | parseInt(byteString) 80 | : byteString; 81 | const e = (Math.log(bytes) / Math.log(1e3)) | 0; 82 | return +(bytes / Math.pow(1e3, e)).toFixed(2) + ' ' + ('kMGTPEZY'[e - 1] || '') + 'B'; 83 | }; 84 | 85 | 86 | /** 87 | * Get path including the last element (e.g. [public, test, index.html]) 88 | */ 89 | const getPathFromUrl = (urlString: string): string[] => { 90 | const url = new URL(urlString); 91 | return url.pathname.split('/').filter(val => val !== ''); 92 | } -------------------------------------------------------------------------------- /src/Components/Dialogs/CreateFile/CreateFile.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import TextField from '@material-ui/core/TextField'; 4 | import Dialog from '@material-ui/core/Dialog'; 5 | import DialogActions from '@material-ui/core/DialogActions'; 6 | import DialogContent from '@material-ui/core/DialogContent'; 7 | import DialogTitle from '@material-ui/core/DialogTitle'; 8 | import { connect } from 'react-redux'; 9 | import { createFile, closeDialog, MyDispatch } from '../../../Actions/Actions'; 10 | import { AppState } from '../../../Reducers/reducer'; 11 | import { DialogStateProps, DialogDispatchProps, DialogButtonClickEvent } from '../dialogTypes'; 12 | import { DIALOGS } from '../../../Actions/actionTypes'; 13 | 14 | class FormDialog extends Component { 15 | private textField: React.RefObject = createRef(); 16 | 17 | handleSubmit(event: DialogButtonClickEvent) { 18 | const textField = this.textField.current; 19 | if (textField) { 20 | const fileName = textField.value; 21 | this.props.handleSubmit(event, { fileName }); 22 | } 23 | } 24 | 25 | render() { 26 | const { handleClose, open } = this.props; 27 | 28 | return ( 29 | 30 |
31 | Create file 32 | 33 | 34 | 35 | 36 | 39 | 42 | 43 |
44 |
45 | ); 46 | } 47 | } 48 | 49 | interface DispatchProps extends DialogDispatchProps { 50 | handleSubmit(event: DialogButtonClickEvent, { fileName }: { fileName: string }): void; 51 | } 52 | interface CreateFileProps extends DialogStateProps, DispatchProps { } 53 | 54 | const mapStateToProps = (state: AppState): DialogStateProps => { 55 | return { 56 | open: state.visibleDialogs.CREATE_FILE 57 | }; 58 | }; 59 | 60 | const mapDispatchToProps = (dispatch: MyDispatch): DispatchProps => { 61 | return { 62 | handleClose: () => { 63 | dispatch(closeDialog(DIALOGS.CREATE_FILE)); 64 | }, 65 | handleSubmit: (event, { fileName }) => { 66 | event.preventDefault(); 67 | dispatch(createFile(fileName)); 68 | } 69 | }; 70 | }; 71 | 72 | export default connect(mapStateToProps, mapDispatchToProps)(FormDialog); 73 | -------------------------------------------------------------------------------- /src/Components/Dialogs/CreateFolder/CreateFolder.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import TextField from '@material-ui/core/TextField'; 4 | import Dialog from '@material-ui/core/Dialog'; 5 | import DialogActions from '@material-ui/core/DialogActions'; 6 | import DialogContent from '@material-ui/core/DialogContent'; 7 | import DialogTitle from '@material-ui/core/DialogTitle'; 8 | import { connect } from 'react-redux'; 9 | import { createNewFolder, closeDialog, MyDispatch } from '../../../Actions/Actions'; 10 | import { DialogStateProps, DialogDispatchProps, DialogButtonClickEvent } from '../dialogTypes'; 11 | import { AppState } from '../../../Reducers/reducer'; 12 | import { DIALOGS } from '../../../Actions/actionTypes'; 13 | 14 | class FormDialog extends Component { 15 | private textField: React.RefObject = createRef(); 16 | 17 | handleSubmit(event: DialogButtonClickEvent) { 18 | const textField = this.textField.current; 19 | if (textField) { 20 | const folderName = textField.value; 21 | this.props.handleSubmit(event, { folderName }); 22 | } 23 | } 24 | 25 | render() { 26 | const { handleClose, open } = this.props; 27 | 28 | return ( 29 | 30 |
31 | Create folder 32 | 33 | 34 | 35 | 36 | 39 | 42 | 43 |
44 |
45 | ); 46 | } 47 | } 48 | 49 | interface DispatchProps extends DialogDispatchProps { 50 | handleSubmit(event: DialogButtonClickEvent, { folderName }: { folderName: string }): void; 51 | } 52 | interface CreateFolderProps extends DialogStateProps, DispatchProps { } 53 | 54 | const mapStateToProps = (state: AppState): DialogStateProps => { 55 | return { 56 | open: state.visibleDialogs.CREATE_FOLDER 57 | }; 58 | }; 59 | 60 | const mapDispatchToProps = (dispatch: MyDispatch): DispatchProps => { 61 | return { 62 | handleClose: () => { 63 | dispatch(closeDialog(DIALOGS.CREATE_FOLDER)); 64 | }, 65 | handleSubmit: (event, { folderName }) => { 66 | event.preventDefault(); 67 | dispatch(createNewFolder(folderName)); 68 | } 69 | }; 70 | }; 71 | 72 | export default connect(mapStateToProps, mapDispatchToProps)(FormDialog); 73 | -------------------------------------------------------------------------------- /src/Components/Dialogs/Media/Media.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import Dialog from '@material-ui/core/Dialog'; 4 | import DialogActions from '@material-ui/core/DialogActions'; 5 | import DialogContent from '@material-ui/core/DialogContent'; 6 | import DialogTitle from '@material-ui/core/DialogTitle'; 7 | import { connect } from 'react-redux'; 8 | import { MyDispatch, closeDialog } from '../../../Actions/Actions'; 9 | import Plyr from 'react-plyr'; 10 | import 'plyr/dist/plyr.css'; 11 | import { FileItem } from '../../../Api/Item'; 12 | import { DialogStateProps, DialogDispatchProps } from '../dialogTypes'; 13 | import { AppState } from '../../../Reducers/reducer'; 14 | import { DIALOGS } from '../../../Actions/actionTypes'; 15 | 16 | class FormDialog extends Component { 17 | render() { 18 | const { file, handleClose, open } = this.props; 19 | 20 | const fileName = file ? file.name : undefined; 21 | const url = file ? file.url : undefined; 22 | // TODO: const provider = file ? (file.isVideo() ? 'html5' : 'audio') : ''; 23 | const type = file ? (file.isVideo() ? 'video' : 'audio') : undefined; 24 | 25 | return ( 26 | 27 | Display Media 28 | 29 | { 30 | file ? 31 | ( 32 |
33 |

Playing {fileName}

34 | 35 |
36 | ) 37 | :

No media file opened

38 | 39 | } 40 |
41 | 42 | 45 | 46 |
47 | ); 48 | } 49 | } 50 | 51 | interface StateProps extends DialogStateProps { 52 | file?: FileItem; 53 | } 54 | interface MediaProps extends StateProps, DialogDispatchProps {} 55 | 56 | 57 | const mapStateToProps = (state: AppState): StateProps => { 58 | const open = state.visibleDialogs.MEDIA; 59 | 60 | const file = state.items.selected[0]; 61 | 62 | if (file instanceof FileItem) { 63 | return { 64 | open, 65 | file, 66 | }; 67 | } 68 | return { open }; 69 | }; 70 | 71 | const mapDispatchToProps = (dispatch: MyDispatch): DialogDispatchProps => { 72 | return { 73 | handleClose: () => { 74 | dispatch(closeDialog(DIALOGS.MEDIA)); 75 | } 76 | }; 77 | }; 78 | 79 | export default connect(mapStateToProps, mapDispatchToProps)(FormDialog); 80 | -------------------------------------------------------------------------------- /cypress/integration/file_spec.js: -------------------------------------------------------------------------------- 1 | describe('file operations', () => { 2 | beforeEach('login user', () => { 3 | cy.createRandomAccount().as('user') 4 | }) 5 | 6 | it('can create text file', function () { 7 | cy.login(this.user) 8 | cy.get('[aria-label=More]').click() 9 | cy.contains('Create file').click() 10 | cy.focused().type('pandas.txt{enter}') 11 | 12 | // enter file content 13 | // workaround because textarea somehow gets rendered multiple times 14 | cy.wait(5000) 15 | cy.get('textarea').type('Pandas are cool') 16 | cy.contains('Update').click() 17 | cy.contains('Update').should('not.exist') 18 | 19 | // displays created file 20 | cy.contains('pandas.txt') 21 | }) 22 | 23 | it('can read a text file', function () { 24 | const fileName = 'file.txt' 25 | const fileUrl = `${this.user.podUrl}/${fileName}` 26 | const fileContent = 'some cool content' 27 | cy.givenTextFile(this.user, fileUrl, fileContent) 28 | cy.login(this.user) 29 | 30 | cy.contains(fileName).dblclick() 31 | cy.contains(fileContent) 32 | }) 33 | 34 | it('can copy a text file to a different folder', function () { 35 | const fileName = 'file.txt' 36 | const fileUrl = `${this.user.podUrl}/${fileName}` 37 | const fileContent = 'some cool content' 38 | const folderName = 'some folder' 39 | const folderUrl = `${this.user.podUrl}/${folderName}` 40 | cy.givenTextFile(this.user, fileUrl, fileContent) 41 | cy.givenFolder(this.user, folderUrl) 42 | cy.login(this.user) 43 | 44 | cy.intercept({ 45 | method: 'PUT', 46 | url: `${this.user.podUrl}/${encodeURIComponent(folderName)}/${fileName}` 47 | }).as('putNewFile') 48 | cy.intercept({ 49 | method: 'GET', 50 | url: `${this.user.podUrl}/${encodeURIComponent(folderName)}` 51 | }).as('getTargetFolder') 52 | 53 | // open copy menu 54 | cy.contains(fileName).rightclick() 55 | cy.contains('Copy').click() 56 | 57 | // select target folder and copy 58 | // TODO: fix %20 in application 59 | cy.contains('some%20folder').click() 60 | cy.wait('@getTargetFolder').its('response.body').should('not.include', fileName).then(cy.log) 61 | cy.get('button').contains('Copy').click() 62 | cy.wait('@putNewFile').then(({ request, response }) => { 63 | expect(request.body).to.equal(fileContent) 64 | expect(request.headers).to.include({ 65 | 'content-type': 'text/plain' 66 | }) 67 | expect(response).to.have.property('statusCode', 201) 68 | }) 69 | 70 | // open target folder in file explorer 71 | cy.contains(folderName).dblclick() 72 | cy.wait('@getTargetFolder').its('response.body').should('include', fileName).then(cy.log) 73 | cy.location('search').should('include', encodeURIComponent(encodeURIComponent(folderName))) 74 | 75 | // open copied file 76 | // TODO: fix double url encoding 77 | cy.contains(fileName).dblclick() 78 | cy.contains(fileContent) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /src/Components/Dialogs/Rename/Rename.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import TextField from '@material-ui/core/TextField'; 4 | import Dialog from '@material-ui/core/Dialog'; 5 | import DialogActions from '@material-ui/core/DialogActions'; 6 | import DialogContent from '@material-ui/core/DialogContent'; 7 | import DialogTitle from '@material-ui/core/DialogTitle'; 8 | import { connect } from 'react-redux'; 9 | import { renameFile, renameFolder, MyDispatch, closeDialog } from '../../../Actions/Actions'; 10 | import { FolderItem, Item } from '../../../Api/Item'; 11 | import { DialogStateProps, DialogDispatchProps, DialogButtonClickEvent } from '../dialogTypes'; 12 | import { AppState } from '../../../Reducers/reducer'; 13 | import { DIALOGS } from '../../../Actions/actionTypes'; 14 | class FormDialog extends Component { 15 | private textField: React.RefObject = createRef(); 16 | 17 | handleSubmit(event: DialogButtonClickEvent) { 18 | const textField = this.textField.current; 19 | const item = this.props.item; 20 | if (textField && item) { 21 | const newName = textField.value; 22 | this.props.handleSubmit(event, { item, newName }); 23 | } 24 | } 25 | 26 | render() { 27 | const { handleClose, open, item } = this.props; 28 | const previousName = item ? item.name : ''; 29 | 30 | return ( 31 | 32 |
33 | Rename 34 | 35 | 36 | 37 | 38 | 41 | 44 | 45 |
46 |
47 | ); 48 | } 49 | } 50 | 51 | interface StateProps extends DialogStateProps { 52 | item?: Item; 53 | } 54 | interface DispatchProps extends DialogDispatchProps { 55 | handleSubmit(event: DialogButtonClickEvent, { item, newName }: { item: Item, newName: string }): void; 56 | } 57 | interface RenameProps extends StateProps, DispatchProps {} 58 | 59 | 60 | const mapStateToProps = (state: AppState): StateProps => { 61 | return { 62 | open: state.visibleDialogs.RENAME, 63 | item: state.items.selected[0], 64 | }; 65 | }; 66 | 67 | const mapDispatchToProps = (dispatch: MyDispatch): DispatchProps => { 68 | return { 69 | handleClose: () => { 70 | dispatch(closeDialog(DIALOGS.RENAME)); 71 | }, 72 | handleSubmit: (event, { item, newName }) => { 73 | event.preventDefault(); 74 | if (item instanceof FolderItem) // TODO: Create renameItem 75 | dispatch(renameFolder(item.name, newName)); 76 | else 77 | dispatch(renameFile(item.name, newName)); 78 | } 79 | }; 80 | }; 81 | 82 | export default connect(mapStateToProps, mapDispatchToProps)(FormDialog); 83 | -------------------------------------------------------------------------------- /src/Components/Dialogs/UploadFile/UploadFile.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import Dialog from '@material-ui/core/Dialog'; 4 | import DialogActions from '@material-ui/core/DialogActions'; 5 | import DialogContent from '@material-ui/core/DialogContent'; 6 | import DialogTitle from '@material-ui/core/DialogTitle'; 7 | import LinearProgress from '@material-ui/core/LinearProgress'; 8 | import { connect } from 'react-redux'; 9 | import { resetFileUploader, uploadFiles, setFileUploadList, MyDispatch, resetFileUploadList } from '../../../Actions/Actions'; 10 | import FileUploader from '../../FileUploader/FileUploader'; 11 | import { DialogStateProps, DialogDispatchProps } from '../dialogTypes'; 12 | import { AppState } from '../../../Reducers/reducer'; 13 | 14 | class FormDialog extends Component { 15 | 16 | render() { 17 | const { handleClose, handleReset, handleSubmit, open, canUpload, progress, fileList, handleSelectedFiles } = this.props; 18 | 19 | return ( 20 | 21 |
22 | 23 | Upload files 24 | 25 | 26 | 27 | {canUpload ? : null } 28 | 29 | 30 | 33 | 36 | 37 |
38 |
39 | ); 40 | } 41 | } 42 | 43 | interface StateProps extends DialogStateProps { 44 | canUpload: boolean; 45 | fileList: FileList|null; 46 | progress: number; 47 | } 48 | interface DispatchProps extends DialogDispatchProps { 49 | handleSelectedFiles(event: React.ChangeEvent): void; 50 | handleReset(): void; 51 | } 52 | interface UploadFileProps extends StateProps, DispatchProps {} 53 | 54 | 55 | const mapStateToProps = (state: AppState): StateProps => { 56 | return { 57 | open: state.visibleDialogs.UPLOAD_FILE, 58 | canUpload: state.upload.fileList ? state.upload.fileList.length > 0 : false, 59 | fileList: state.upload.fileList, 60 | progress: state.upload.progress, 61 | }; 62 | }; 63 | 64 | const mapDispatchToProps = (dispatch: MyDispatch): DispatchProps => { 65 | return { 66 | handleClose: (event) => { 67 | dispatch(resetFileUploader()); 68 | }, 69 | handleSubmit: (event) => { 70 | event.preventDefault(); 71 | dispatch(uploadFiles()); 72 | }, 73 | handleSelectedFiles: (event) => { 74 | const files = event.target.files; 75 | dispatch(setFileUploadList(files)); 76 | }, 77 | handleReset: () => { 78 | dispatch(resetFileUploadList()); 79 | } 80 | }; 81 | }; 82 | 83 | export default connect(mapStateToProps, mapDispatchToProps)(FormDialog); 84 | -------------------------------------------------------------------------------- /src/Components/Dialogs/Settings/Settings.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import Dialog from '@material-ui/core/Dialog'; 4 | import DialogActions from '@material-ui/core/DialogActions'; 5 | import DialogContent from '@material-ui/core/DialogContent'; 6 | import DialogTitle from '@material-ui/core/DialogTitle'; 7 | import { connect } from 'react-redux'; 8 | import { MyDispatch, closeDialog, toggleWithAcl, toggleWithMeta } from '../../../Actions/Actions'; 9 | import { DialogDispatchProps, DialogStateProps } from '../dialogTypes'; 10 | import { AppState } from '../../../Reducers/reducer'; 11 | import { DIALOGS } from '../../../Actions/actionTypes'; 12 | import { FormControlLabel, FormGroup, Switch } from '@material-ui/core'; 13 | 14 | class FormDialog extends Component { 15 | state = { 16 | withAcl: false, 17 | withMeta: false, 18 | }; 19 | 20 | componentWillReceiveProps(props: SettingsProps) { 21 | const { withAcl, withMeta } = props; 22 | this.setState({ withAcl, withMeta }) 23 | } 24 | 25 | handleChange(event: React.ChangeEvent) { 26 | this.setState({ location: event.target.value }) 27 | } 28 | 29 | handleToggleWithAcl() { 30 | this.props.handleToggleWithAcl(); 31 | } 32 | 33 | handleToggleWithMeta() { 34 | this.props.handleToggleWithMeta(); 35 | } 36 | 37 | render() { 38 | let { withAcl, withMeta } = this.state; 39 | const { handleClose, open } = this.props; 40 | 41 | return ( 42 | 43 |
44 | Settings 45 | 46 | 47 | } /> 48 | } /> 49 | 50 | 51 | 52 | 55 | 56 |
57 |
58 | ); 59 | } 60 | } 61 | 62 | interface StateProps extends DialogStateProps { 63 | withAcl: boolean; 64 | withMeta: boolean; 65 | } 66 | interface DispatchProps extends DialogDispatchProps { 67 | handleToggleWithAcl(): void; 68 | handleToggleWithMeta(): void; 69 | } 70 | interface SettingsProps extends StateProps, DispatchProps { } 71 | 72 | 73 | const mapStateToProps = (state: AppState): StateProps => { 74 | return { 75 | open: state.visibleDialogs.SETTINGS, 76 | withAcl: state.settings.withAcl, 77 | withMeta: state.settings.withMeta, 78 | }; 79 | }; 80 | 81 | const mapDispatchToProps = (dispatch: MyDispatch): DispatchProps => { 82 | return { 83 | handleClose: () => dispatch(closeDialog(DIALOGS.SETTINGS)), 84 | handleToggleWithAcl: () => dispatch(toggleWithAcl()), 85 | handleToggleWithMeta: () => dispatch(toggleWithMeta()), 86 | }; 87 | }; 88 | 89 | export default connect(mapStateToProps, mapDispatchToProps)(FormDialog); 90 | -------------------------------------------------------------------------------- /src/Components/Notification/NotificationBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import CheckCircleIcon from '@material-ui/icons/CheckCircle'; 4 | import ErrorIcon from '@material-ui/icons/Error'; 5 | import InfoIcon from '@material-ui/icons/Info'; 6 | import green from '@material-ui/core/colors/green'; 7 | import amber from '@material-ui/core/colors/amber'; 8 | import SnackbarContent from '@material-ui/core/SnackbarContent'; 9 | import WarningIcon from '@material-ui/icons/Warning'; 10 | import { withStyles, createStyles, Theme, WithStyles } from '@material-ui/core/styles'; 11 | import { connect } from 'react-redux'; 12 | import { AppState } from '../../Reducers/reducer'; 13 | 14 | 15 | enum VariantTypes { 16 | SUCCESS = 'success', 17 | WARNING = 'warning', 18 | ERROR = 'error', 19 | INFO = 'info' 20 | } 21 | 22 | const variantIcon = { 23 | [VariantTypes.SUCCESS]: CheckCircleIcon, 24 | [VariantTypes.WARNING]: WarningIcon, 25 | [VariantTypes.ERROR]: ErrorIcon, 26 | [VariantTypes.INFO]:InfoIcon, 27 | }; 28 | 29 | const styles1 = (theme: Theme) => createStyles({ 30 | [VariantTypes.SUCCESS]: { 31 | backgroundColor: green[600], 32 | }, 33 | [VariantTypes.ERROR]: { 34 | backgroundColor: theme.palette.error.dark, 35 | }, 36 | [VariantTypes.INFO]: { 37 | backgroundColor: theme.palette.primary.dark, 38 | }, 39 | [VariantTypes.WARNING]: { 40 | backgroundColor: amber[700], 41 | }, 42 | icon: { 43 | fontSize: 20, 44 | }, 45 | iconVariant: { 46 | opacity: 0.9, 47 | marginRight: theme.spacing(), 48 | }, 49 | message: { 50 | display: 'flex', 51 | alignItems: 'center', 52 | }, 53 | }); 54 | 55 | function MySnackbarContent(props: MySnackbarContentProps) { 56 | const { open, classes, className, message, variant, ...other } = props; 57 | const Icon = variantIcon[variant]; 58 | 59 | return ( 60 | open ? 61 | 66 | 67 | {message} 68 | 69 | } 70 | {...other} 71 | /> 72 | : <> 73 | ); 74 | } 75 | 76 | interface MySnackbarContentProps extends WithStyles { 77 | open: boolean; 78 | className: string; 79 | variant: VariantTypes; 80 | message: string; 81 | [k: string]: any; 82 | } 83 | 84 | const MySnackbarContentWrapper = withStyles(styles1)(MySnackbarContent); 85 | 86 | const styles2 = (theme: Theme) => createStyles({ 87 | margin: { 88 | margin: theme.spacing(), 89 | }, 90 | }); 91 | 92 | function CustomizedSnackbars(props: CustomizedSnackbarsProps) { 93 | const { classes, open, errorMsg } = props; 94 | return ( 95 | 101 | ); 102 | } 103 | 104 | interface StateProps { 105 | open: boolean; 106 | errorMsg: string; 107 | } 108 | interface CustomizedSnackbarsProps extends StateProps, WithStyles {} 109 | 110 | const mapStateToProps = (state: AppState): StateProps => { 111 | return { 112 | open: !!state.errorMessage, 113 | errorMsg: state.errorMessage, 114 | }; 115 | }; 116 | 117 | const mapDispatchToProps = () => ({}); 118 | 119 | export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles2)(CustomizedSnackbars)); 120 | 121 | 122 | -------------------------------------------------------------------------------- /src/Components/ContextMenu/ContextMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import './ContextMenu.css'; 4 | import Menu from '@material-ui/core/Menu'; 5 | import OpenAction from './ContextMenuActions/OpenAction'; 6 | import RemoveAction from './ContextMenuActions/RemoveAction'; 7 | import MoveAction from './ContextMenuActions/MoveAction'; 8 | import CopyAction from './ContextMenuActions/CopyAction'; 9 | import EditAction from './ContextMenuActions/EditAction'; 10 | import RenameAction from './ContextMenuActions/RenameAction'; 11 | import ZipAction from './ContextMenuActions/ZipAction'; 12 | import ExtractAction from './ContextMenuActions/ExtractAction'; 13 | import DownloadAction from './ContextMenuActions/DownloadAction'; 14 | import OpenInNewTabAction from './ContextMenuActions/OpenInNewTabAction'; 15 | import { Item, FileItem, FolderItem } from '../../Api/Item'; 16 | import { AppState } from '../../Reducers/reducer'; 17 | 18 | class ContextMenu extends Component { 19 | 20 | render() { 21 | const { acts, open, x, y } = this.props; 22 | 23 | return ( 24 |
25 | { }} 34 | PaperProps={{ style: { width: 190 } }}> 35 | {acts.includes('open') && } 36 | {acts.includes('openInNewTab') && } 37 | {acts.includes('download') && } 38 | {acts.includes('compress') && } 39 | {acts.includes('extract') && } 40 | {acts.includes('edit') && } 41 | {acts.includes('copy') && } 42 | {acts.includes('move') && } 43 | {acts.includes('rename') && } 44 | {acts.includes('remove') && } 45 | 46 |
47 | ); 48 | } 49 | } 50 | 51 | interface StateProps { 52 | acts: string[]; 53 | open: boolean; 54 | x: number; 55 | y: number; 56 | } 57 | interface ContextMenuProps extends StateProps {} 58 | 59 | 60 | const mapStateToProps = (state: AppState): StateProps => { 61 | return { 62 | x: state.contextMenu.x, 63 | y: state.contextMenu.y, 64 | open: state.contextMenu.open, 65 | acts: getActionsForMultipleItems(state.items.selected), 66 | }; 67 | }; 68 | 69 | const mapDispatchToProps = () => ({}); 70 | 71 | 72 | /** 73 | * Get available actions for multiple items 74 | */ 75 | const getActionsForMultipleItems = (items: Item[]): string[] => { 76 | return items.length === 1 ? 77 | getActionsForItem(items[0]) 78 | : [ 79 | 'copy', 80 | 'move', 81 | 'remove', 82 | 'download', 83 | 'compress', 84 | ]; 85 | }; 86 | 87 | /** 88 | * Get available actions for an item 89 | */ 90 | const getActionsForItem = (item: Item) => { 91 | const commonActions = [ 92 | 'openInNewTab', 93 | 'copy', 94 | 'move', 95 | 'rename', 96 | 'remove', 97 | 'download', 98 | ]; 99 | return [ 100 | ...commonActions, 101 | ...((item instanceof FileItem) ? 102 | getActionsForFile(item) 103 | : getActionsForFolder(item)) 104 | ]; 105 | }; 106 | 107 | /** 108 | * Get available file specific actions 109 | */ 110 | const getActionsForFile = (file: FileItem) => { 111 | const actions = []; 112 | file.isEditable() && actions.push('edit'); 113 | file.isExtractable() && actions.push('extract'); 114 | (file.isImage() || file.isMedia()) && actions.push('open'); 115 | 116 | return actions; 117 | }; 118 | 119 | /** 120 | * Get available folder specific actions 121 | */ 122 | const getActionsForFolder = (folder: FolderItem) => { 123 | return [ 124 | 'open', 125 | 'compress' 126 | ]; 127 | }; 128 | 129 | 130 | export default connect(mapStateToProps, mapDispatchToProps)(ContextMenu); -------------------------------------------------------------------------------- /src/Components/Dialogs/ChooseLocation/ChooseLocation.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import Dialog from '@material-ui/core/Dialog'; 4 | import DialogActions from '@material-ui/core/DialogActions'; 5 | import DialogContent from '@material-ui/core/DialogContent'; 6 | import DialogTitle from '@material-ui/core/DialogTitle'; 7 | import FileListSublist from '../../FileList/FileListSublist/FileListSublist'; 8 | import KeyboardArrowLeftIcon from '@material-ui/icons/KeyboardArrowLeft'; 9 | import { Item, FolderItem } from '../../../Api/Item'; 10 | import * as ApiHandler from '../../../Api/ApiHandler'; 11 | 12 | class FormDialog extends Component { 13 | private host: string; 14 | private path: string[]; 15 | 16 | constructor(props: OwnProps) { 17 | super(props); 18 | const { initialPath, initialHost } = props; 19 | this.host = initialHost; 20 | this.path = initialPath; 21 | 22 | this.state = { 23 | items: [], 24 | isLoading: true, 25 | wasPreviouslyOpen: false, 26 | }; 27 | } 28 | 29 | componentDidUpdate(prevProps: OwnProps) { 30 | if (prevProps.initialHost !== this.props.initialHost 31 | || prevProps.initialPath.join('') !== this.props.initialPath.join('')) { 32 | this.host = this.props.initialHost; 33 | this.path = this.props.initialPath; 34 | } 35 | if (this.props.open && !this.state.wasPreviouslyOpen) { 36 | this.setState({ wasPreviouslyOpen: true }) 37 | this.updateItems() 38 | } 39 | if (!this.props.open && this.state.wasPreviouslyOpen) { 40 | this.setState({ wasPreviouslyOpen: false }) 41 | } 42 | } 43 | 44 | handleGoBack() { 45 | this.path = this.path.slice(0, -1); 46 | this.updateItems(); 47 | } 48 | 49 | handleOpenFolder(folder: FolderItem) { 50 | this.path = [...folder.path, folder.name]; 51 | this.updateItems(); 52 | } 53 | 54 | async updateItems() { 55 | this.setState({ isLoading: true }); 56 | const items = (await ApiHandler.getItemList(this.path.join('/'))) 57 | .filter(item => item instanceof FolderItem); 58 | 59 | this.setState({ isLoading: false, items }); 60 | } 61 | 62 | render() { 63 | const { open, handleClose, handleSubmit, actionName } = this.props; 64 | const { items, isLoading } = this.state; 65 | const host = this.host; 66 | const path = this.path; 67 | const url = `${host}/${path.join('/')}`; 68 | const canGoBack = path.length > 0; 69 | 70 | return ( 71 | 72 |
73 | 74 | {actionName} items to { url } 75 | 76 | 77 | 78 | 79 | 80 | 83 | 84 | 87 | 90 | 91 |
92 |
93 | ); 94 | } 95 | } 96 | 97 | interface OwnProps { 98 | open: boolean; 99 | actionName: string; 100 | initialHost: string; 101 | initialPath: string[]; 102 | handleSubmit({ host, path }: { host: string, path: string[] }): void; 103 | handleClose(): void; 104 | } 105 | 106 | interface OwnState { 107 | items: Item[]; 108 | isLoading: boolean; 109 | wasPreviouslyOpen: boolean; 110 | } 111 | 112 | export default FormDialog; 113 | -------------------------------------------------------------------------------- /src/Components/HistoryHandler/HistoryHandler.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import { createBrowserHistory, History, Location } from "history"; 4 | import { MyDispatch, setHost, enterFolder } from "../../Actions/Actions"; 5 | import { AppState } from "../../Reducers/reducer"; 6 | 7 | class HistoryHandler extends Component { 8 | private history: History; 9 | private states: LocationState[]; 10 | private stateIndex: number; 11 | 12 | constructor(props: HistoryHandlerProps) { 13 | super(props); 14 | 15 | this.states = []; 16 | this.stateIndex = -1; 17 | this.history = createBrowserHistory(); 18 | this.history.listen((location, action) => { 19 | switch(action) { 20 | case 'POP': 21 | this.handlePop(location); 22 | break; 23 | case 'REPLACE': 24 | this.handleReplace(location); 25 | break; 26 | case 'PUSH': 27 | this.handlePush(location); 28 | break; 29 | } 30 | }); 31 | } 32 | 33 | componentDidUpdate() { 34 | const { host, path } = this.props; 35 | 36 | // Don't update history when the host is invalid 37 | if (host === null) 38 | return; 39 | if (this.states.length === 0 || this.stateIndex < 0) 40 | return this.updateBrowserHistory(); 41 | 42 | const prevState = this.states[this.stateIndex]; 43 | 44 | if (!locationsEqual({ host, path }, prevState)) 45 | this.updateBrowserHistory(); 46 | } 47 | 48 | updateBrowserHistory() { 49 | const { host, path } = this.props; 50 | const url = encodeURI(`${host}/${path.join('/')}`); 51 | const newState = { 52 | host: host || '', 53 | path, 54 | index: this.stateIndex + 1, 55 | }; 56 | 57 | this.history.push(`?url=${url}`, newState); 58 | } 59 | 60 | handlePop(location: Location) { 61 | this.stateIndex = location.state.index; 62 | this.props.handlePop(location); 63 | } 64 | 65 | handleReplace(location: Location) { 66 | this.states[this.stateIndex] = location.state; 67 | } 68 | 69 | handlePush(location: Location) { 70 | this.states = [...this.states.slice(0, ++this.stateIndex), location.state]; 71 | } 72 | 73 | render() { 74 | // This Component doesn't provide anything to the DOM 75 | // The only reason it is a component is to get access to the state and dispatch 76 | return <>; 77 | } 78 | } 79 | 80 | interface LocationState extends MyLocation { 81 | index: number; 82 | } 83 | interface MyLocation { 84 | host: string; 85 | path: string[]; 86 | } 87 | 88 | 89 | interface StateProps { 90 | host: string | null; 91 | path: string[]; 92 | } 93 | interface DispatchProps { 94 | handlePop(location: Location): void; 95 | } 96 | interface HistoryHandlerProps extends StateProps, DispatchProps { } 97 | 98 | 99 | const mapStateToProps = (state: AppState): StateProps => ({ 100 | host: state.account.host, 101 | path: state.path 102 | }); 103 | 104 | const mapDispatchToProps = (dispatch: MyDispatch): DispatchProps => { 105 | return { 106 | handlePop: (location: Location) => { 107 | let host = ''; 108 | let path: string[] = []; 109 | 110 | if (location && typeof location.state !== typeof undefined) { 111 | ({ host, path } = location.state); 112 | } 113 | else { 114 | const params = new URLSearchParams(location.search.substr(1)); 115 | const url = params.get('url'); 116 | if (url !== null) { 117 | ({ host, path } = getLocationObjectFromUrl(url)); 118 | } 119 | } 120 | dispatch(setHost(host)); 121 | dispatch(enterFolder(path)); 122 | } 123 | }; 124 | }; 125 | 126 | export const getLocationObjectFromUrl = (urlString: string) => { 127 | const url = new URL(urlString); 128 | const host = url.origin; 129 | const path = url.pathname.split('/').filter(val => val !== ''); 130 | 131 | return { 132 | host, 133 | path 134 | }; 135 | } 136 | 137 | const locationsEqual = (first: MyLocation, second: MyLocation) => { 138 | return first.host === second.host 139 | && first.path.length === second.path.length 140 | && first.path.every((val, index) => val === second.path[index]); 141 | } 142 | 143 | export default connect(mapStateToProps, mapDispatchToProps)(HistoryHandler); 144 | -------------------------------------------------------------------------------- /src/Components/File/File.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { 4 | setSelectedItemsFromLastTo, loadAndEditFile, loadAndDisplayFile, displaySelectedMediaFile, 5 | rightClickOnFile, enterFolderByItem, MyDispatch, openContextMenu, toggleSelectedItem, selectItems 6 | } from '../../Actions/Actions'; 7 | import './File.css'; 8 | 9 | import ListItem from '@material-ui/core/ListItem'; 10 | import ListItemAvatar from '@material-ui/core/ListItemAvatar'; 11 | import ListItemText from '@material-ui/core/ListItemText'; 12 | import Avatar from '@material-ui/core/Avatar'; 13 | import FolderIcon from '@material-ui/icons/Folder'; 14 | import FileIcon from '@material-ui/icons/InsertDriveFile'; 15 | import blue from '@material-ui/core/colors/blue'; 16 | import { FileItem, Item } from '../../Api/Item'; 17 | import { AppState } from '../../Reducers/reducer'; 18 | 19 | class File extends Component { 20 | render() { 21 | const { isSelected, item, handleClick, handleDoubleClick, handleContextMenu } = this.props; 22 | const avatarStyle = { 23 | backgroundColor: isSelected ? blue['A200'] : undefined 24 | }; 25 | const realSize = (item instanceof FileItem) ? item.getDisplaySize() : null; 26 | return ( 27 |
28 | 29 | 30 | 31 | { (item instanceof FileItem) ? : } 32 | 33 | 34 | 35 | 36 |
37 | ); 38 | } 39 | } 40 | 41 | 42 | interface FileOwnProps { 43 | item: Item; 44 | } 45 | interface StateProps { 46 | isSelected: boolean; 47 | } 48 | interface DispatchProps { 49 | handleClick(event: React.MouseEvent): void; 50 | handleDoubleClick(): void; 51 | handleContextMenu(event: React.MouseEvent): void; 52 | } 53 | interface FileProps extends FileOwnProps, StateProps, DispatchProps {} 54 | 55 | 56 | const mapStateToProps = (state: AppState, ownProps: FileOwnProps): StateProps => { 57 | return { 58 | isSelected: state.items.selected.includes(ownProps.item) 59 | }; 60 | }; 61 | 62 | 63 | const mapDispatchToProps = (dispatch: MyDispatch, ownProps: FileOwnProps): DispatchProps => { 64 | return { 65 | handleDoubleClick: () => { 66 | const item = ownProps.item; 67 | 68 | if (item instanceof FileItem) { 69 | if (item.isEditable()) 70 | dispatch(loadAndEditFile(item.name)); 71 | else if (item.isImage()) 72 | dispatch(loadAndDisplayFile(item.name)); 73 | else if (item.isMedia()) 74 | dispatch(displaySelectedMediaFile()); 75 | } 76 | else 77 | dispatch(enterFolderByItem(item)); 78 | }, 79 | 80 | handleContextMenu: (event: React.MouseEvent | React.TouchEvent) => { 81 | event.preventDefault(); 82 | event.stopPropagation(); 83 | 84 | let x = 0; 85 | let y = 0; 86 | 87 | if (event.nativeEvent instanceof MouseEvent) { 88 | x = event.nativeEvent.clientX; 89 | y = event.nativeEvent.clientY; 90 | } 91 | else if (event.nativeEvent instanceof TouchEvent) { 92 | x = event.nativeEvent.touches[0].pageX; 93 | y = event.nativeEvent.touches[0].pageY; 94 | } 95 | else { 96 | console.warn("Unknown click event", event); 97 | } 98 | 99 | if (event.shiftKey) { 100 | dispatch(setSelectedItemsFromLastTo(ownProps.item)); 101 | } else { 102 | dispatch(rightClickOnFile(ownProps.item)); 103 | } 104 | 105 | dispatch(openContextMenu({ x, y })); 106 | }, 107 | 108 | handleClick: (event: React.MouseEvent | React.TouchEvent) => { 109 | event.stopPropagation(); 110 | 111 | if (event.ctrlKey) { 112 | dispatch(toggleSelectedItem(ownProps.item)); 113 | } else if (event.shiftKey) { 114 | dispatch(setSelectedItemsFromLastTo(ownProps.item)); 115 | } else { 116 | dispatch(selectItems([ownProps.item])); 117 | } 118 | } 119 | }; 120 | }; 121 | 122 | export default connect(mapStateToProps, mapDispatchToProps)(File); 123 | -------------------------------------------------------------------------------- /src/Components/Dialogs/Edit/Edit.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import Dialog from '@material-ui/core/Dialog'; 4 | import DialogActions from '@material-ui/core/DialogActions'; 5 | import DialogContent from '@material-ui/core/DialogContent'; 6 | import DialogContentText from '@material-ui/core/DialogContentText'; 7 | import DialogTitle from '@material-ui/core/DialogTitle'; 8 | import { connect } from 'react-redux'; 9 | import { updateTextFile, MyDispatch, closeDialog } from '../../../Actions/Actions'; 10 | import { DialogStateProps, DialogDispatchProps, DialogButtonClickEvent } from '../dialogTypes'; 11 | import { AppState } from '../../../Reducers/reducer'; 12 | import { DIALOGS } from '../../../Actions/actionTypes'; 13 | import { Item } from '../../../Api/Item'; 14 | 15 | class FormDialog extends Component { 16 | private textField: React.RefObject = createRef(); 17 | state = { 18 | lastBlobUrl: null as string|null, 19 | content: null as string|null, 20 | contentType: null as string|null, 21 | loading: false 22 | }; 23 | 24 | componentDidUpdate() { 25 | if (this.props.blobUrl !== this.state.lastBlobUrl) { 26 | this.setState({ 27 | lastBlobUrl: this.props.blobUrl 28 | }); 29 | this.setState({ 30 | loading: true 31 | }); 32 | 33 | this.props.blobUrl && fetch(this.props.blobUrl).then(async r => { 34 | this.setState({ 35 | content: await r.text(), 36 | contentType: r.headers.get('content-type') 37 | }); 38 | this.setState({ 39 | loading: false 40 | }); 41 | }); 42 | } 43 | } 44 | 45 | handleSave(event: DialogButtonClickEvent) { 46 | event.preventDefault(); 47 | const textField = this.textField.current; 48 | const item = this.props.item; 49 | if (textField && item) { 50 | const content = textField.value; 51 | const contentType = this.state.contentType ? this.state.contentType : 'text/plain'; 52 | this.props.handleSubmit(event, { 53 | itemName: item.name, 54 | content, 55 | contentType 56 | }); 57 | } 58 | } 59 | 60 | render() { 61 | const { handleClose, open, item } = this.props; 62 | const itemName = item ? item.getDisplayName() : 'No item selected'; 63 | const textAreaStyle = { 64 | width: '100%', 65 | minHeight: '300px' 66 | }; 67 | const textArea =