├── src
├── index.scss
├── softwares
│ ├── Explorer
│ │ ├── components
│ │ │ ├── Directory
│ │ │ │ ├── Directory.scss
│ │ │ │ └── Directory.jsx
│ │ │ ├── Icon
│ │ │ │ └── Icon.jsx
│ │ │ ├── Sidebar
│ │ │ │ ├── Sidebar.scss
│ │ │ │ └── Sidebar.jsx
│ │ │ └── Ribbon
│ │ │ │ ├── Ribbon.scss
│ │ │ │ └── Ribbon.jsx
│ │ ├── Explorer.scss
│ │ └── Explorer.jsx
│ ├── Calender
│ │ ├── Calender.scss
│ │ └── Calender.jsx
│ ├── Settings
│ │ ├── Settings.scss
│ │ └── Settings.jsx
│ ├── Calculator
│ │ ├── Calculator.scss
│ │ └── Calculator.jsx
│ └── Notepad
│ │ ├── Notepad.scss
│ │ └── Notepad.jsx
├── redux
│ ├── boot
│ │ ├── boot.selectors.js
│ │ ├── boot.types.js
│ │ ├── boot.actions.js
│ │ ├── boot.utils.js
│ │ └── index.js
│ ├── store.js
│ ├── memory
│ │ ├── memory.types.js
│ │ ├── memory.selectors.js
│ │ ├── memory.action.js
│ │ ├── memory.utils.js
│ │ └── index.js
│ ├── auth
│ │ ├── auth.types.js
│ │ ├── auth.user.js
│ │ ├── auth.selectors.js
│ │ ├── auth.actions.js
│ │ ├── auth.utils.js
│ │ └── index.js
│ ├── reducer.js
│ └── account
│ │ ├── account.types.js
│ │ ├── account.selectors.js
│ │ ├── account.fs.js
│ │ ├── account.actions.js
│ │ ├── account.utils.js
│ │ └── index.js
├── components
│ ├── Clock
│ │ ├── Clock.scss
│ │ └── Clock.jsx
│ ├── Button
│ │ ├── Button.scss
│ │ └── Button.jsx
│ ├── Loading
│ │ ├── Loading.jsx
│ │ └── Loading.scss
│ ├── LockCover
│ │ ├── LockCover.scss
│ │ └── LockCover.jsx
│ ├── BootLogo
│ │ ├── BootLogo.jsx
│ │ └── BootLogo.scss
│ ├── TitleBar
│ │ ├── TitleBar.scss
│ │ └── TitleBar.jsx
│ ├── MenuBar
│ │ ├── MenuBar.jsx
│ │ └── MenuBar.scss
│ ├── StartMenu
│ │ ├── Promotions.jsx
│ │ ├── StartMenu.scss
│ │ └── StartMenu.jsx
│ └── TaskBar
│ │ ├── TaskBar.jsx
│ │ └── TaskBar.scss
├── pages
│ ├── SwitchUser
│ │ ├── SwitchUser.scss
│ │ ├── SwitchUser.jsx
│ │ ├── LoginView.scss
│ │ └── LoginView.jsx
│ ├── Desktop
│ │ ├── Desktop.scss
│ │ └── Desktop.jsx
│ ├── BootScreen
│ │ ├── BootScreen.scss
│ │ └── BootScreen.jsx
│ ├── Program
│ │ ├── Program.scss
│ │ └── Program.jsx
│ └── NewAccount
│ │ ├── NewAccount.scss
│ │ ├── ChildView.scss
│ │ ├── ChildView.jsx
│ │ └── NewAccount.jsx
├── setupTests.js
├── App.test.js
├── App.scss
├── scss
│ ├── _variables.scss
│ └── _mixins.scss
├── reportWebVitals.js
├── config
│ ├── wallpapers.js
│ ├── strings.js
│ └── apps.js
├── disk
│ ├── disk.read.js
│ ├── disk.delete.js
│ ├── disk.write.js
│ ├── disk.connect.js
│ ├── disk.dispatch.js
│ └── index.js
├── index.js
├── app.util.js
├── App.js
└── logo.svg
├── .env
├── .vscode
└── settings.json
├── .babelrc
├── public
├── favicon.ico
├── logo192.png
├── logo512.png
├── robots.txt
├── fonts
│ ├── OpenSans-Bold.ttf
│ ├── OpenSans-Italic.ttf
│ ├── OpenSans-Light.ttf
│ ├── OpenSans-Regular.ttf
│ └── OpenSans-SemiBold.ttf
├── images
│ ├── 9fa80fd805562a6bc817f01f48b8b93e.jpg
│ └── d2luMTBfb2ZmaWNpYWxfd2FsbHBhcGVy.jpg
├── icons
│ ├── MiFolder.svg
│ └── MiDocument.svg
├── manifest.json
├── index.css
└── index.html
├── .prettierrc
├── .gitignore
├── LICENSE
├── .eslintrc.json
├── package.json
└── README.md
/src/index.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | SKIP_PREFLIGHT_CHECK=true
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.eol": "\n"
3 | }
--------------------------------------------------------------------------------
/src/softwares/Explorer/components/Directory/Directory.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react"]
3 | }
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piyush078/mindows/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piyush078/mindows/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piyush078/mindows/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/fonts/OpenSans-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piyush078/mindows/HEAD/public/fonts/OpenSans-Bold.ttf
--------------------------------------------------------------------------------
/public/fonts/OpenSans-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piyush078/mindows/HEAD/public/fonts/OpenSans-Italic.ttf
--------------------------------------------------------------------------------
/public/fonts/OpenSans-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piyush078/mindows/HEAD/public/fonts/OpenSans-Light.ttf
--------------------------------------------------------------------------------
/public/fonts/OpenSans-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piyush078/mindows/HEAD/public/fonts/OpenSans-Regular.ttf
--------------------------------------------------------------------------------
/public/fonts/OpenSans-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piyush078/mindows/HEAD/public/fonts/OpenSans-SemiBold.ttf
--------------------------------------------------------------------------------
/public/images/9fa80fd805562a6bc817f01f48b8b93e.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piyush078/mindows/HEAD/public/images/9fa80fd805562a6bc817f01f48b8b93e.jpg
--------------------------------------------------------------------------------
/public/images/d2luMTBfb2ZmaWNpYWxfd2FsbHBhcGVy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piyush078/mindows/HEAD/public/images/d2luMTBfb2ZmaWNpYWxfd2FsbHBhcGVy.jpg
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "useTabs": false,
4 | "singleQuote": true,
5 | "printWidth": 100,
6 | "editor.formatOnSave": true
7 | }
8 |
--------------------------------------------------------------------------------
/src/redux/boot/boot.selectors.js:
--------------------------------------------------------------------------------
1 | const bootState = (state) => state.boot;
2 |
3 | export const selectBootBackgrounds = (store) => bootState(store).backgrounds;
4 |
--------------------------------------------------------------------------------
/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import { createStore } from 'redux';
2 | import reducer from './reducer';
3 |
4 | const store = createStore(reducer);
5 | export default store;
6 |
--------------------------------------------------------------------------------
/src/components/Clock/Clock.scss:
--------------------------------------------------------------------------------
1 | .Clock {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | color: white;
6 | font-size: 14px;
7 | margin-top: -6px;
8 | }
--------------------------------------------------------------------------------
/src/redux/memory/memory.types.js:
--------------------------------------------------------------------------------
1 | const MemoryActionTypes = {
2 | START_NEW_PROGRAM: 'START_NEW_PROGRAM',
3 | TERMINATE_PROGRAM: 'TERMINATE_PROGRAM',
4 | };
5 |
6 | export default MemoryActionTypes;
7 |
--------------------------------------------------------------------------------
/src/pages/SwitchUser/SwitchUser.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/mixins';
2 |
3 | .new-account-opt {
4 | @include mixins.fonttype('semibold');
5 | text-decoration: none;
6 | color: white;
7 | margin-top: 16px;
8 | }
--------------------------------------------------------------------------------
/src/pages/Desktop/Desktop.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/mixins';
2 |
3 | .Desktop {
4 | @include mixins.fullscreen;
5 | @include mixins.wallpaperbg;
6 | position: absolute;
7 | top: 0;
8 | left: 0;
9 | transition: opacity .5s ease;
10 | }
--------------------------------------------------------------------------------
/src/redux/auth/auth.types.js:
--------------------------------------------------------------------------------
1 | const AuthActionTypes = {
2 | CHECK_USER_SESSION: 'CHECK_USER_SESSION',
3 | CREATE_NEW_ACCOUNT: 'CREATE_NEW_ACCOUNT',
4 | SIGN_IN: 'SIGN_IN',
5 | LOGOUT_USER: 'LOGOUT_USER',
6 | };
7 |
8 | export default AuthActionTypes;
9 |
--------------------------------------------------------------------------------
/src/redux/memory/memory.selectors.js:
--------------------------------------------------------------------------------
1 | const memoryState = (store) => store.memory;
2 |
3 | export const selectAppsInstances = (store) => memoryState(store).appsInstances;
4 |
5 | export const selectProgramsData = (store) => memoryState(store).programsData;
6 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/redux/boot/boot.types.js:
--------------------------------------------------------------------------------
1 | const BootActionTypes = {
2 | GET_BACKGROUNDS: 'GET_BACKGROUNDS',
3 | SET_LOCK_SCREEN_BACKGROUND: 'SET_LOwCK_SCREEN_BACKGROUND',
4 | SET_LOGIN_SCREEN_BACKGROUND: 'SET_LOGIN_SCREEN_BACKGROUND',
5 | };
6 |
7 | export default BootActionTypes;
8 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render();
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/src/softwares/Explorer/components/Icon/Icon.jsx:
--------------------------------------------------------------------------------
1 | const Icon = ({ isDir }) =>
2 | isDir ? (
3 |
4 | ) : (
5 |
6 | );
7 |
8 | export default Icon;
9 |
--------------------------------------------------------------------------------
/src/components/Button/Button.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/mixins';
2 |
3 | .Button {
4 | @include mixins.fonttype;
5 | width: 200px;
6 | font-size: 16px;
7 | color: white;
8 | padding-top: 8px;
9 | padding-bottom: 8px;
10 | outline: none;
11 | border: 0px;
12 | }
13 |
14 |
15 | .Button:disabled {
16 | opacity: .5;
17 | }
--------------------------------------------------------------------------------
/src/App.scss:
--------------------------------------------------------------------------------
1 | @use './scss/mixins';
2 |
3 | * {
4 | @include mixins.reset;
5 | box-sizing: border-box;
6 | }
7 |
8 | :root {
9 | font-size: .9rem;
10 | }
11 |
12 | html, body, #root, .App {
13 | @include mixins.reset;
14 | @include mixins.fullscreen;
15 |
16 | overflow: hidden !important;
17 | }
18 |
19 | @include mixins.transitionFadeIn;
--------------------------------------------------------------------------------
/src/components/Button/Button.jsx:
--------------------------------------------------------------------------------
1 | import './Button.scss';
2 |
3 | const Button = ({ styles, classNames, text, ...props }) => (
4 |
12 | );
13 |
14 | export default Button;
15 |
--------------------------------------------------------------------------------
/src/redux/auth/auth.user.js:
--------------------------------------------------------------------------------
1 | export const User = (name, username, password) => ({
2 | name,
3 | username,
4 | password,
5 | });
6 |
7 | export const PublicUser = (user) => ({
8 | name: user.name,
9 | username: user.username,
10 | });
11 |
12 | export const getName = (user) => user.name;
13 | export const getUsername = (user) => user.username;
14 |
--------------------------------------------------------------------------------
/src/scss/_variables.scss:
--------------------------------------------------------------------------------
1 | $defaultFont: 'Segoe UI', 'Open Sans';
2 | $defaultFontSize: 16px;
3 |
4 | $taskBarBg: #091418;
5 | $taskBarFg: white;
6 | $taskBarHoverBg: #122830f3;
7 |
8 | $titleBarHeight: 40px;
9 | $titleBarBg: white;
10 |
11 | $itemHoverBg: #d5ecfc;
12 | $itemFocusBg: #cce8fe;
13 | $itemBorderColor: #8ecbfa;
14 | $menuBarBorder: #f1f1f1;
15 |
--------------------------------------------------------------------------------
/src/redux/memory/memory.action.js:
--------------------------------------------------------------------------------
1 | import MemoryActionTypes from './memory.types';
2 |
3 | export const startNewProgram = (app, metadata) => ({
4 | type: MemoryActionTypes.START_NEW_PROGRAM,
5 | payload: { app, metadata },
6 | });
7 |
8 | export const terminateProgram = (pId) => ({
9 | type: MemoryActionTypes.TERMINATE_PROGRAM,
10 | payload: pId,
11 | });
12 |
--------------------------------------------------------------------------------
/src/redux/auth/auth.selectors.js:
--------------------------------------------------------------------------------
1 | const authState = (state) => state.auth;
2 |
3 | export const selectUsers = (store) => authState(store).users;
4 |
5 | export const selectActiveUser = (store) => authState(store).activeUser;
6 |
7 | export const selectAuthError = (store) => authState(store).error;
8 |
9 | export const selectAuthSuccess = (store) => authState(store).success;
10 |
--------------------------------------------------------------------------------
/public/icons/MiFolder.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/BootScreen/BootScreen.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/mixins';
2 |
3 | .BootScreen {
4 | @include mixins.fullscreen;
5 | position: absolute;
6 | background: rgb(1, 1, 16);
7 | top: 0;
8 | left: 0;
9 |
10 | display: flex;
11 | flex-direction: column;
12 |
13 | > * {
14 | margin: auto;
15 | }
16 |
17 | .brandlogo {
18 | color: lightblue;
19 | font-size: 160px;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.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 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/src/redux/boot/boot.actions.js:
--------------------------------------------------------------------------------
1 | import BootActionTypes from './boot.types';
2 |
3 | export const getBootBackgrounds = () => ({
4 | type: BootActionTypes.GET_BACKGROUNDS,
5 | });
6 |
7 | export const setLockScreenBackground = () => ({
8 | type: BootActionTypes.SET_LOCK_SCREEN_BACKGROUND,
9 | });
10 |
11 | export const setLoginScreenBackground = () => ({
12 | type: BootActionTypes.SET_LOGIN_SCREEN_BACKGROUND,
13 | });
14 |
--------------------------------------------------------------------------------
/src/redux/reducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | import AuthReducer from './auth';
4 | import BootReducer from './boot';
5 | import AccountReducer from './account';
6 | import MemoryReducer from './memory';
7 |
8 | const reducer = combineReducers({
9 | auth: AuthReducer,
10 | boot: BootReducer,
11 | account: AccountReducer,
12 | memory: MemoryReducer,
13 | });
14 |
15 | export default reducer;
16 |
--------------------------------------------------------------------------------
/src/pages/Program/Program.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/mixins';
2 |
3 | .Program {
4 | position: absolute;
5 | top: 10%;
6 | left: 30%;
7 | background: white;
8 | flex-direction: column;
9 | box-shadow: 0px 0px 5px 2px #2d2d2d;
10 | transition: top 0.3s ease-out, right 0.3s ease-out, bottom 0.3s ease-out, left 0.3s ease-out;
11 |
12 | &-component {
13 | @include mixins.fullscreen;
14 | position: relative;
15 | }
16 | }
--------------------------------------------------------------------------------
/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = (onPerfEntry) => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/src/config/wallpapers.js:
--------------------------------------------------------------------------------
1 | const Wallpapers = {
2 | list: [
3 | {
4 | name: '9fa80fd805562a6bc817f01f48b8b93e.jpg',
5 | description: 'Wharariki Beach Cave, Archway Islands, South Island of New Zealand',
6 | },
7 | {
8 | name: 'd2luMTBfb2ZmaWNpYWxfd2FsbHBhcGVy.jpg',
9 | description: 'Windows 10 Official wallpaper',
10 | },
11 | ],
12 | defaultWallpaper: 1,
13 | };
14 |
15 | export default Wallpapers;
16 |
--------------------------------------------------------------------------------
/src/pages/BootScreen/BootScreen.jsx:
--------------------------------------------------------------------------------
1 | import { GrWindows } from 'react-icons/gr';
2 |
3 | import BootLogo from '../../components/BootLogo/BootLogo';
4 | import './BootScreen.scss';
5 |
6 | const BootScreen = () => (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | );
16 |
17 | export default BootScreen;
18 |
--------------------------------------------------------------------------------
/src/redux/account/account.types.js:
--------------------------------------------------------------------------------
1 | const AccountActionTypes = {
2 | LOAD_ACCOUNT: 'LOAD_ACCOUNT',
3 | GET_ACCOUNT_SETTINGS: 'GET_ACCOUNT_SETTINGS',
4 | GET_FILESYSTEM: 'GET_FILESYSTEM',
5 | SAVE_ACCOUNT: 'SAVE_ACCOUNT',
6 |
7 | CREATE_NEW_DIR_ITEM: 'CREATE_NEW_DIR_ITEM',
8 | RENAME_DIR_ITEM: 'RENAME_DIR_ITEM',
9 | DELETE_DIR_ITEM: 'DELETE_DIR_ITEM',
10 | COPY_DIR_ITEM: 'COPY_DIR_ITEM',
11 | MOVE_DIR_ITEM: 'MOVE_DIR_ITEM',
12 | };
13 |
14 | export default AccountActionTypes;
15 |
--------------------------------------------------------------------------------
/src/components/Loading/Loading.jsx:
--------------------------------------------------------------------------------
1 | import BootLogo from '../BootLogo/BootLogo';
2 | import './Loading.scss';
3 |
4 | const Loading = (props) => {
5 | const { message = 'Loading...', background = '#1f1e49' } = props;
6 | const style = { background };
7 |
8 | return (
9 |
15 | );
16 | };
17 |
18 | export default Loading;
19 |
--------------------------------------------------------------------------------
/src/softwares/Explorer/components/Sidebar/Sidebar.scss:
--------------------------------------------------------------------------------
1 | .Explorer-sidebar {
2 | position: sticky;
3 | top: 0;
4 | width: 144px;
5 | height: 100%;
6 | font-size: 13px;
7 | border-right: 1px solid lightgray;
8 | cursor: default;
9 |
10 | &-item {
11 | display: flex;
12 | padding: 4px 8px;
13 |
14 | &-subitems {
15 | > * {
16 | padding-left: 16px;
17 | }
18 | }
19 |
20 | & span:first-of-type {
21 | font-size: 1.25em;
22 | margin-right: 8px;
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/src/disk/disk.read.js:
--------------------------------------------------------------------------------
1 | const searchInIDB = (key) => (db, STORENAME) =>
2 | new Promise((resolve, reject) => {
3 | if (!key) {
4 | reject(new Error('No key provided to search the store'));
5 | }
6 |
7 | const transaction = db.transaction(STORENAME, 'readonly');
8 | const store = transaction.objectStore(STORENAME);
9 | const query = store.get(key);
10 |
11 | query.onsuccess = () => resolve(query.result);
12 | query.onerror = () => reject(query.error);
13 | });
14 |
15 | export default searchInIDB;
16 |
--------------------------------------------------------------------------------
/src/redux/account/account.selectors.js:
--------------------------------------------------------------------------------
1 | const accountState = (state) => state.account;
2 |
3 | export const selectAccountSettings = (store) => accountState(store).settings;
4 |
5 | export const selectTaskBarApps = (store) => accountState(store).taskbarApps;
6 |
7 | export const selectDirectoryItems = (id) => (store) => {
8 | const fs = accountState(store).filesystem;
9 | return fs[id] ? fs[id].children.map((child) => fs[child].node) : [];
10 | };
11 |
12 | export const selectDefaultApps = (store) => accountState(store).defaultApps;
13 |
--------------------------------------------------------------------------------
/public/icons/MiDocument.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/disk/disk.delete.js:
--------------------------------------------------------------------------------
1 | const deleteFromIDB = (key) => (db, STORENAME) =>
2 | new Promise((resolve, reject) => {
3 | if (!key) {
4 | reject(new Error('No key provided to delete from the store'));
5 | }
6 |
7 | const transaction = db.transaction(STORENAME, 'readwrite');
8 | const store = transaction.objectStore(STORENAME);
9 | const query = store.delete(key);
10 |
11 | query.onsuccess = () => resolve(query.result);
12 | query.onerror = () => reject(query.error);
13 | });
14 |
15 | export default deleteFromIDB;
16 |
--------------------------------------------------------------------------------
/src/redux/auth/auth.actions.js:
--------------------------------------------------------------------------------
1 | import AuthActionTypes from './auth.types';
2 |
3 | export const checkUserSession = () => ({
4 | type: AuthActionTypes.CHECK_USER_SESSION,
5 | });
6 |
7 | export const createNewAccount = (userDetails) => ({
8 | type: AuthActionTypes.CREATE_NEW_ACCOUNT,
9 | payload: userDetails,
10 | });
11 |
12 | export const signInUser = (userIndex, password) => ({
13 | type: AuthActionTypes.SIGN_IN,
14 | payload: { userIndex, password },
15 | });
16 |
17 | export const logOut = () => ({
18 | type: AuthActionTypes.LOGOUT_USER,
19 | });
20 |
--------------------------------------------------------------------------------
/src/disk/disk.write.js:
--------------------------------------------------------------------------------
1 | const writeInIDB = (document) => (db, STORENAME) =>
2 | new Promise((resolve, reject) => {
3 | if (!document.id) {
4 | reject(new Error('No unique key provided to store the document'));
5 | }
6 |
7 | const transaction = db.transaction(STORENAME, 'readwrite');
8 | const store = transaction.objectStore(STORENAME);
9 | const query = store.put(document, document.id);
10 |
11 | query.onsuccess = () => resolve(query.result);
12 | query.onerror = () => reject(query.error);
13 | });
14 |
15 | export default writeInIDB;
16 |
--------------------------------------------------------------------------------
/src/softwares/Calender/Calender.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/mixins';
2 |
3 | .Calender {
4 | @include mixins.fullscreen;
5 | display: flex;
6 |
7 | &-leftbar {
8 | height: 100%;
9 | flex: 1;
10 | border-right: 2px solid lightgray;
11 | display: flex;
12 | flex-direction: column;
13 | padding: 1rem;
14 | font-size: 2.5em;
15 |
16 | svg {
17 | margin-bottom: 1em;
18 | }
19 | }
20 |
21 | &-rightbar {
22 | height: 100%;
23 | flex: 2;
24 | padding: 2rem 1rem;
25 | font-size: 1em;
26 | display: flex;
27 | align-items: center;
28 | }
29 | }
--------------------------------------------------------------------------------
/src/softwares/Settings/Settings.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/mixins';
2 |
3 | .Settings {
4 | @include mixins.fullscreen;
5 | display: flex;
6 |
7 | &-leftbar {
8 | height: 100%;
9 | flex: 1;
10 | border-right: 2px solid lightgray;
11 | display: flex;
12 | flex-direction: column;
13 | padding: 1rem;
14 | font-size: 2.5em;
15 |
16 | svg {
17 | margin-bottom: 1em;
18 | }
19 | }
20 |
21 | &-rightbar {
22 | height: 100%;
23 | flex: 2;
24 | padding: 2rem 1rem;
25 | font-size: 1em;
26 | display: flex;
27 | align-items: center;
28 | }
29 | }
--------------------------------------------------------------------------------
/src/softwares/Calender/Calender.jsx:
--------------------------------------------------------------------------------
1 | import { VscDebug } from 'react-icons/vsc';
2 | import './Calender.scss';
3 |
4 | const Calender = () => (
5 |
6 |
7 |
8 |
Oops! Mindows must be broken!!
9 |
10 |
11 |
12 |
There seems to be no Calender installed on the system.
13 |
But wait... you can always add it and send a pull request.
14 |
15 |
16 |
17 | );
18 |
19 | export default Calender;
20 |
--------------------------------------------------------------------------------
/src/softwares/Settings/Settings.jsx:
--------------------------------------------------------------------------------
1 | import { VscDebug } from 'react-icons/vsc';
2 | import './Settings.scss';
3 |
4 | const Settings = () => (
5 |
6 |
7 |
8 |
Oops! Mindows must be broken!!
9 |
10 |
11 |
12 |
There seems to be no Settings installed on the system.
13 |
But wait... you can always add it and send a pull request.
14 |
15 |
16 |
17 | );
18 |
19 | export default Settings;
20 |
--------------------------------------------------------------------------------
/src/softwares/Calculator/Calculator.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/mixins';
2 |
3 | .Calculator {
4 | @include mixins.fullscreen;
5 | display: flex;
6 |
7 | &-leftbar {
8 | height: 100%;
9 | flex: 1;
10 | border-right: 2px solid lightgray;
11 | display: flex;
12 | flex-direction: column;
13 | padding: 1rem;
14 | font-size: 2.5em;
15 |
16 | svg {
17 | margin-bottom: 1em;
18 | }
19 | }
20 |
21 | &-rightbar {
22 | height: 100%;
23 | flex: 2;
24 | padding: 2rem 1rem;
25 | font-size: 1em;
26 | display: flex;
27 | align-items: center;
28 | }
29 | }
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/Loading/Loading.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/mixins';
2 |
3 | .Loading {
4 | @include mixins.fullscreen;
5 | position: absolute;
6 | top: 0;
7 | left: 0;
8 | font-size: 1em;
9 | display: flex;
10 | justify-content: center;
11 | align-items: center;
12 | flex-direction: column;
13 | transition-property: opacity;
14 | transition-timing-function: ease;
15 | }
16 |
17 | .heading {
18 | text-align: center;
19 | padding-top: 48px;
20 |
21 | p {
22 | color: white;
23 | @include mixins.fonttype('light');
24 | }
25 |
26 | &-subtitle {
27 | font-size: 24px;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/softwares/Notepad/Notepad.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/mixins';
2 |
3 | .Notepad {
4 | @include mixins.fullscreen;
5 | position: relative;
6 | display: flex;
7 | flex-direction: column;
8 |
9 | &-content {
10 | @include mixins.fullscreen;
11 | @include mixins.fonttype;
12 |
13 | &-textarea {
14 | @include mixins.fullscreen;
15 | padding: 8px;
16 | resize: none !important;
17 | outline: none !important;
18 | border: 0 !important;
19 | overflow-y: scroll;
20 | }
21 |
22 | &-textarea:focus {
23 | outline: none !important;
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/softwares/Calculator/Calculator.jsx:
--------------------------------------------------------------------------------
1 | import { VscDebug } from 'react-icons/vsc';
2 | import './Calculator.scss';
3 |
4 | const Calculator = () => (
5 |
6 |
7 |
8 |
Oops! Mindows must be broken!!
9 |
10 |
11 |
12 |
There seems to be no Calculator installed on the system.
13 |
But wait... you can always add it and send a pull request.
14 |
15 |
16 |
17 | );
18 |
19 | export default Calculator;
20 |
--------------------------------------------------------------------------------
/src/components/LockCover/LockCover.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/mixins';
2 |
3 | .LockCover {
4 | @include mixins.fullscreen;
5 | @include mixins.wallpaperbg;
6 | position: absolute;
7 | top: 0;
8 | left: 0;
9 | opacity: 0;
10 | transition: top .5s ease-out, opacity .25s ease;
11 | }
12 |
13 | .LockCoverHidden {
14 | top: -100%;
15 | }
16 |
17 | div.currentTime {
18 | @include mixins.fonttype('light');
19 |
20 | position: absolute;
21 | left: 64px;
22 | bottom: 96px;
23 | color: white;
24 |
25 | p.title {
26 | font-size: 120px;
27 | margin-bottom: -16px;
28 | }
29 |
30 | p.subtitle {
31 | font-size: 64px;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/redux/boot/boot.utils.js:
--------------------------------------------------------------------------------
1 | import Wallpapers from '../../config/wallpapers';
2 |
3 | const storageKeys = {
4 | LOCK_SCREEN_BACKGROUND: 'LOCK_SCREEN_BACKGROUND',
5 | LOGIN_SCREEN_BACKGROUND: 'LOGIN_SCREEN_BACKGROUND',
6 | };
7 |
8 | export const getLockScreenBackground = () => {
9 | const name = localStorage.getItem(storageKeys.LOCK_SCREEN_BACKGROUND);
10 | return name in Wallpapers.list ? name : Wallpapers.list[0].name;
11 | };
12 |
13 | export const getLoginScreenBackground = () => {
14 | const name = localStorage.getItem(storageKeys.LOGIN_SCREEN_BACKGROUND);
15 | return name in Wallpapers.list ? name : Wallpapers.list[Wallpapers.defaultWallpaper].name;
16 | };
17 |
--------------------------------------------------------------------------------
/src/config/strings.js:
--------------------------------------------------------------------------------
1 | const Strings = {
2 | INCORRECT_PASSWORD: 'The password is incorrect. Try again.',
3 | LOGIN_LINK_SIGNUP_VIEW: 'Or, even better, sign-in',
4 | SIGNUP_LINK_LOGIN_VIEW: 'Sign-up options',
5 | SIGNUP_NAME_VIEW_TITLE: "Who's going to use this PC?",
6 | SIGNUP_NAME_VIEW_SUBTITLE: 'What name do you want to use?',
7 | SIGNUP_PASSWORD_VIEW_TITLE: 'Create a super memorable password',
8 | SIGNUP_PASSWORD_VIEW_SUBTITLE: "Make sure to pick something you'll absolutely remember",
9 | SUCCESSFUL_LOGIN_WELCOME_TEXT: 'Welcome',
10 | TASKBAR_SEARCH_PLACEHOLDER: 'Type here to search',
11 | PC_FILESYSTEM_ROOT_NAME: 'MY PC',
12 | };
13 |
14 | export default Strings;
15 |
--------------------------------------------------------------------------------
/src/redux/boot/index.js:
--------------------------------------------------------------------------------
1 | import BootActionTypes from './boot.types';
2 | import { getLockScreenBackground, getLoginScreenBackground } from './boot.utils';
3 |
4 | const initialState = {
5 | backgrounds: { lock: null, login: null },
6 | };
7 |
8 | const BootReducer = (state = initialState, action) => {
9 | switch (action.type) {
10 | case BootActionTypes.GET_BACKGROUNDS: {
11 | const lockBackground = getLockScreenBackground();
12 | const loginBackground = getLoginScreenBackground();
13 | return { ...state, backgrounds: { lock: lockBackground, login: loginBackground } };
14 | }
15 |
16 | default:
17 | return state;
18 | }
19 | };
20 |
21 | export default BootReducer;
22 |
--------------------------------------------------------------------------------
/src/redux/auth/auth.utils.js:
--------------------------------------------------------------------------------
1 | import { PublicUser } from './auth.user';
2 |
3 | const storageKeys = {
4 | PERSISTENT_USERS: 'PERSISTENT_USERS',
5 | };
6 |
7 | export const checkForStoredUser = () => {
8 | const users = localStorage.getItem(storageKeys.PERSISTENT_USERS);
9 | return users ? JSON.parse(users) : [];
10 | };
11 |
12 | export const saveNewUser = (user) => {
13 | const users = checkForStoredUser().concat(user);
14 | localStorage.setItem(storageKeys.PERSISTENT_USERS, JSON.stringify(users));
15 | return users;
16 | };
17 |
18 | export const safeguardUser = (users) => (users ? users.map((user) => PublicUser(user)) : users);
19 |
20 | export const signInUser = (userIndex, password) => {
21 | const users = checkForStoredUser();
22 | return users[userIndex].password === password;
23 | };
24 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import { Provider } from 'react-redux';
5 |
6 | import './index.scss';
7 | import App from './App';
8 | import store from './redux/store';
9 | // import reportWebVitals from './reportWebVitals';
10 |
11 | ReactDOM.render(
12 |
13 |
14 |
15 |
16 |
17 |
18 | ,
19 | document.getElementById('root')
20 | );
21 |
22 | // If you want to start measuring performance in your app, pass a function
23 | // to log results (for example: reportWebVitals(console.log))
24 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
25 | // reportWebVitals();
26 |
--------------------------------------------------------------------------------
/src/disk/disk.connect.js:
--------------------------------------------------------------------------------
1 | const checkBrowserSupport = () =>
2 | window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
3 |
4 | export const indexedDB = checkBrowserSupport();
5 |
6 | export const connectIDB = (DB_NAME, DB_VERSION, STORENAME) => {
7 | const onupgradeneeded = (db) => {
8 | console.log('IndexedDb OnUpgradeNeeded');
9 | if (!db.objectStoreNames.contains(STORENAME)) {
10 | db.createObjectStore(STORENAME);
11 | }
12 | };
13 |
14 | return new Promise((resolve, reject) => {
15 | const request = indexedDB.open(DB_NAME, DB_VERSION);
16 | request.onupgradeneeded = () => onupgradeneeded(request.result);
17 | request.onsuccess = () => resolve(request.result);
18 | request.onerror = () => reject(request.error);
19 | request.onblocked = () => reject(new Error('IndexedDb Blocked'));
20 | });
21 | };
22 |
--------------------------------------------------------------------------------
/src/pages/NewAccount/NewAccount.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/mixins';
2 |
3 | .NewAccount {
4 | @include mixins.fullscreen;
5 | position: absolute;
6 | background: #1f1e49;
7 | top: 0;
8 | left: 0;
9 | display: flex;
10 | flex-direction: column;
11 | }
12 |
13 | .view-header {
14 | width: 100%;
15 | background: #13122b;
16 | padding: 0 16px;
17 | display: flex;
18 | justify-content: space-between;
19 | align-items: center;
20 | color: white;
21 |
22 | > * {
23 | padding: 8px 0;
24 | }
25 |
26 | > svg {
27 | font-size: 32px;
28 | }
29 |
30 | &-title {
31 | @include mixins.fonttype('semibold');
32 | text-align: center;
33 | width: 320px;
34 | color: white;
35 | border-bottom: 2px solid white;
36 | }
37 | }
38 |
39 | .view-content {
40 | @include mixins.fullscreen;
41 | height: calc(100% - 40px);
42 | position: relative;
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/BootLogo/BootLogo.jsx:
--------------------------------------------------------------------------------
1 | import './BootLogo.scss';
2 |
3 | /**
4 | * The following code is taken from codepen created by Fernando de Almeida Faria.
5 | * https://codepen.io/feebaa/pen/PPrLQP
6 | */
7 |
8 | const BootLogo = (props) => {
9 | const { size = 'medium' } = { ...props };
10 | const componentSizes = {
11 | loader: size === 'medium' ? '48px' : '32px',
12 | circle: size === 'medium' ? '32px' : '24px',
13 | };
14 |
15 | const loaderStyle = { height: componentSizes.loader, width: componentSizes.loader };
16 | const circleStyle = { height: componentSizes.circle, width: componentSizes.circle };
17 |
18 | return (
19 |
20 | {[...Array(5)].map((_, i) => (
21 |
22 | ))}
23 |
24 | );
25 | };
26 |
27 | export default BootLogo;
28 |
--------------------------------------------------------------------------------
/public/index.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Open Sans';
3 | src: url('/fonts/OpenSans-Regular.ttf') format('ttf');
4 | }
5 |
6 | @font-face {
7 | font-family: 'Open Sans Light';
8 | src: url('/fonts/OpenSans-Light.ttf') format('ttf');
9 | }
10 |
11 | @font-face {
12 | font-family: 'Open Sans Semibold';
13 | src: url('/fonts/OpenSans-SemiBold.ttf') format('ttf');
14 | }
15 |
16 | @font-face {
17 | font-family: 'Open Sans Bold';
18 | src: url('/fonts/OpenSans-Bold.ttf') format('ttf');
19 | }
20 |
21 | @font-face {
22 | font-family: 'Open Sans Italic';
23 | src: url('/fonts/OpenSans-Italic.ttf') format('ttf');
24 | }
25 |
26 | html, body {
27 | background: rgb(1, 1, 16);
28 | font-family: 'Segoe UI', 'Open Sans';
29 | -webkit-touch-callout: none;
30 | -webkit-user-select: none;
31 | -khtml-user-select: none;
32 | -moz-user-select: none;
33 | -ms-user-select: none;
34 | user-select: none;
35 | }
--------------------------------------------------------------------------------
/src/components/Clock/Clock.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { subscribeClock, unsubscribeClock, formatDateTime } from '../../app.util';
3 | import './Clock.scss';
4 |
5 | const dateOptions = { weekday: 'short', month: 'long', day: 'numeric' };
6 | const timeOptions = { hour12: true, hour: 'numeric', minute: '2-digit', hour12suffix: true };
7 |
8 | const Clock = () => {
9 | const [currentDateTime, updateCurrentDateTime] = useState(new Date());
10 | const dateTimeFormat = formatDateTime(currentDateTime, dateOptions, timeOptions);
11 |
12 | // set time interval for current time clock
13 | useEffect(() => {
14 | const clockRef = subscribeClock(updateCurrentDateTime);
15 | return () => unsubscribeClock(clockRef);
16 | }, []);
17 |
18 | return (
19 |
20 |
{dateTimeFormat.time}
21 |
{dateTimeFormat.date}
22 |
23 | );
24 | };
25 |
26 | export default Clock;
27 |
--------------------------------------------------------------------------------
/src/app.util.js:
--------------------------------------------------------------------------------
1 | export const addKeyListener = (handler, on = 'keydown') => {
2 | document.addEventListener(on, handler);
3 | };
4 |
5 | export const removeKeyListener = (handler, on = 'keydown') => {
6 | document.removeEventListener(on, handler);
7 | };
8 |
9 | export const subscribeClock = (onUpdate) => {
10 | const currentDateTime = new Date();
11 | let clockRef = setTimeout(() => {
12 | onUpdate(new Date());
13 | clockRef = setInterval(() => onUpdate(new Date()), 1000 * 60);
14 | }, (60 - currentDateTime.getSeconds()) * 1000);
15 | return clockRef;
16 | };
17 |
18 | export const unsubscribeClock = (clockRef) => {
19 | clearInterval(clockRef);
20 | };
21 |
22 | export const formatDateTime = (dateTime, dateOptions, timeOptions) => {
23 | const time = dateTime.toLocaleTimeString([], timeOptions);
24 | const date = dateTime.toLocaleDateString([], dateOptions);
25 | return {
26 | date,
27 | time: timeOptions.hour12suffix ? time : time.replace('AM', '').replace('PM', ''),
28 | };
29 | };
30 |
--------------------------------------------------------------------------------
/src/disk/disk.dispatch.js:
--------------------------------------------------------------------------------
1 | import { indexedDB, connectIDB } from './disk.connect';
2 |
3 | const dispatchIDB = async ({ DB_NAME, DB_VERSION }, STORENAME, action) =>
4 | new Promise((resolve, reject) => {
5 | let db = null;
6 | try {
7 | if (!indexedDB) {
8 | reject(new Error('IndexedDb not supported in the browser'));
9 | } else if (!DB_NAME || !DB_VERSION || !STORENAME || !action) {
10 | reject(new Error('Required values not provided. Cannot perform the action.'));
11 | } else {
12 | connectIDB(DB_NAME, DB_VERSION, STORENAME)
13 | .then((connectRes) => {
14 | db = connectRes;
15 | action(db, STORENAME)
16 | .then((data) => resolve(data))
17 | .catch((error) => reject(error));
18 | })
19 | .catch((error) => reject(error));
20 | }
21 | } catch (err) {
22 | reject(err);
23 | } finally {
24 | if (db) db.close();
25 | }
26 | });
27 |
28 | export default dispatchIDB;
29 |
--------------------------------------------------------------------------------
/src/components/TitleBar/TitleBar.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/mixins';
2 | @import '../../scss/variables';
3 |
4 | .TitleBar {
5 | display: flex;
6 | justify-content: space-between;
7 | align-items: center;
8 | width: 100%;
9 | height: $titleBarHeight;
10 | background: $titleBarBg;
11 |
12 | &-details {
13 | height: 100%;
14 | display: flex;
15 | align-items: center;
16 | padding: 0 8px;
17 |
18 | &-logo {
19 | font-size: 20px;
20 | }
21 |
22 | &-title {
23 | @include mixins.fonttype;
24 | margin-left: 8px;
25 | font-size: 13px;
26 | }
27 | }
28 |
29 | &-buttons {
30 | height: 100%;
31 | display: flex;
32 | align-items: center;
33 | justify-content: flex-end;
34 |
35 | > * {
36 | height: 100%;
37 | width: $titleBarHeight;
38 | font-size: 1.0em;
39 | padding: .75em;
40 | }
41 |
42 | & > *:hover {
43 | background: beige;
44 | }
45 |
46 | &-close:hover {
47 | background: crimson;
48 | color: white;
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/disk/index.js:
--------------------------------------------------------------------------------
1 | import dispatchIDB from './disk.dispatch';
2 | import writeInIDB from './disk.write';
3 | import searchInIDB from './disk.read';
4 | import deleteFromIDB from './disk.delete';
5 |
6 | export const updateProgramData = (DISK) => (STORENAME, data, callback) => {
7 | dispatchIDB(DISK, STORENAME, writeInIDB(data))
8 | .then((res) => callback && callback({ data: res }))
9 | .catch((err) => callback && callback({ error: err }));
10 | };
11 |
12 | export const getProgramData = (DISK) => (STORENAME, key, callback) => {
13 | dispatchIDB(DISK, STORENAME, searchInIDB(key))
14 | .then((res) => callback && callback({ data: res }))
15 | .catch((err) => callback && callback({ error: err }));
16 | };
17 |
18 | export const deleteProgramData = (DISK) => (STORENAME, key, callback) => {
19 | dispatchIDB(DISK, STORENAME, deleteFromIDB(key))
20 | .then((res) => callback && callback({ data: res }))
21 | .catch((err) => callback && callback({ error: err }));
22 | };
23 |
24 | export const initDisk = (DB_NAME, DB_VERSION) => ({
25 | DB_NAME,
26 | DB_VERSION,
27 | });
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Piyush Madhusudan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/redux/memory/memory.utils.js:
--------------------------------------------------------------------------------
1 | export const loadProgram = (program, metadata = {}) => {
2 | const title = metadata.title || program.config?.initTitle || program.name;
3 | const uniqueId = new Date().getTime().toString() + Math.floor(Math.random() * 100).toString();
4 |
5 | return { pId: uniqueId, ...metadata, title, ...program };
6 | };
7 |
8 | export const updateProgramsData = (data, newProgram) => {
9 | const newData = { ...data };
10 | newData[newProgram.pId] = newProgram;
11 | return newData;
12 | };
13 |
14 | export const updateAppsInstances = (data, newProgram) => {
15 | const newInstances = { ...data };
16 | const key = newProgram.id;
17 | if (!(key in data)) newInstances[key] = [];
18 | newInstances[key] = [...newInstances[key], newProgram.pId];
19 | return newInstances;
20 | };
21 |
22 | export const removeProgram = (data, pId) => {
23 | const newData = { ...data };
24 | delete newData[pId];
25 | return newData;
26 | };
27 |
28 | export const removeAppInstance = (data, id, pId) => {
29 | const newData = {};
30 | newData[id] = data[id].filter((instanceId) => instanceId !== pId);
31 | return Object.assign(data, newData);
32 | };
33 |
--------------------------------------------------------------------------------
/src/redux/memory/index.js:
--------------------------------------------------------------------------------
1 | import MemoryActionTypes from './memory.types';
2 | import {
3 | loadProgram,
4 | removeAppInstance,
5 | removeProgram,
6 | updateAppsInstances,
7 | updateProgramsData,
8 | } from './memory.utils';
9 |
10 | const initialState = {
11 | appsInstances: {},
12 | programsData: {},
13 | };
14 |
15 | const MemoryReducer = (state = initialState, action) => {
16 | switch (action.type) {
17 | case MemoryActionTypes.START_NEW_PROGRAM: {
18 | const { app, metadata } = action.payload;
19 | const newProgram = loadProgram(app, metadata);
20 | return {
21 | ...state,
22 | programsData: updateProgramsData(state.programsData, newProgram),
23 | appsInstances: updateAppsInstances(state.appsInstances, newProgram),
24 | };
25 | }
26 |
27 | case MemoryActionTypes.TERMINATE_PROGRAM: {
28 | const pId = action.payload;
29 | const { id } = state.programsData[pId];
30 | return {
31 | ...state,
32 | programsData: removeProgram(state.programsData, pId),
33 | appsInstances: removeAppInstance(state.appsInstances, id, pId),
34 | };
35 | }
36 |
37 | default:
38 | return state;
39 | }
40 | };
41 |
42 | export default MemoryReducer;
43 |
--------------------------------------------------------------------------------
/src/components/TitleBar/TitleBar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | VscChromeClose,
4 | VscChromeMinimize,
5 | VscChromeMaximize,
6 | VscChromeRestore,
7 | } from 'react-icons/vsc';
8 | import './TitleBar.scss';
9 |
10 | const TitleBar = React.memo(
11 | ({ app, programId, title, isMaximized, onMinimize, onMaximize, onRestore, onTerminate }) => (
12 |
13 |
14 |
{app.icon()}
15 |
{title}
16 |
17 |
18 |
19 |
20 |
21 |
22 | {isMaximized ? : }
23 |
24 |
25 |
26 |
27 |
28 |
29 | ),
30 | (prevProps, nextProps) => prevProps.isMaximized === nextProps.isMaximized
31 | );
32 |
33 | export default TitleBar;
34 |
--------------------------------------------------------------------------------
/src/scss/_mixins.scss:
--------------------------------------------------------------------------------
1 | @import './variables';
2 |
3 | @mixin reset {
4 | margin: 0;
5 | padding: 0;
6 | }
7 |
8 | @mixin fullscreen {
9 | height: 100%;
10 | width: 100%;
11 | }
12 |
13 | @mixin wallpaperbg {
14 | background-size: cover;
15 | background-repeat: no-repeat;
16 | background-position: center center;
17 | background-attachment: fixed;
18 | }
19 |
20 | @mixin fonttype($type: 'regular') {
21 | @if $type == 'light' {
22 | font-family: 'Segoe UI Light', 'Open Sans Light';
23 | } @else if $type == 'bold' {
24 | font-family: 'Segoe UI Bold', 'Open Sans Bold';
25 | } @else if $type == 'semibold' {
26 | font-family: 'Segoe UI Semibold', 'Open Sans Semibold';
27 | } @else {
28 | font-family: $defaultFont;
29 | }
30 | }
31 |
32 | @mixin overlay {
33 | content: "";
34 | position: absolute;
35 | background: rgba(0,0,0,.6);
36 | top: 0;
37 | left: 0;
38 | bottom: 0;
39 | right: 0;
40 | }
41 |
42 | @mixin transitionFadeIn {
43 | .fade {
44 | &-enter {
45 | opacity: 0;
46 | }
47 |
48 | &-enter-active {
49 | opacity: 1;
50 | transition: opacity 200ms;
51 | }
52 |
53 | &-exit {
54 | opacity: 1;
55 | }
56 |
57 | &-exit-active {
58 | opacity: 0;
59 | transition: opacity 200ms;
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/src/redux/account/account.fs.js:
--------------------------------------------------------------------------------
1 | const generateRandomId = (len) => {
2 | const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
3 | return [...Array(len)]
4 | .map(() => characters[Math.floor(Math.random() * characters.length)])
5 | .join('');
6 | };
7 |
8 | const Directory = (name, modifiedTime, createdTime) => ({
9 | id: generateRandomId(5),
10 | name,
11 | modifiedTime: modifiedTime || new Date().getTime(),
12 | createdTime: createdTime || new Date().getTime(),
13 | isDir: true,
14 | });
15 |
16 | const File = (name, modifiedTime, createdTime) => ({
17 | id: generateRandomId(5),
18 | name: name.indexOf('.') !== -1 ? name.substr(0, name.lastIndexOf('.')) : name,
19 | extension: name.indexOf('.') !== -1 ? name.substring(name.lastIndexOf('.') + 1) : '',
20 | modifiedTime: modifiedTime || new Date().getTime(),
21 | createdTime: createdTime || new Date().getTime(),
22 | isDir: false,
23 | });
24 |
25 | const Node = (name, isDir, parent, children = [], mTime = null, id = null) => {
26 | const time = mTime || new Date().getTime();
27 | const node = isDir ? Directory(name, time, time) : File(name, time, time);
28 | node.id = id || node.id;
29 | return {
30 | node,
31 | parent,
32 | children: [...children],
33 | };
34 | };
35 |
36 | export default Node;
37 |
--------------------------------------------------------------------------------
/src/redux/account/account.actions.js:
--------------------------------------------------------------------------------
1 | import AccountActionTypes from './account.types';
2 |
3 | export const loadAccount = (activeUser) => ({
4 | type: AccountActionTypes.LOAD_ACCOUNT,
5 | payload: activeUser,
6 | });
7 |
8 | export const saveAccount = (activeUser) => ({
9 | type: AccountActionTypes.SAVE_ACCOUNT,
10 | payload: activeUser,
11 | });
12 |
13 | export const getAccountSettings = () => ({
14 | type: AccountActionTypes.GET_ACCOUNT_SETTINGS,
15 | });
16 |
17 | export const getAccountFileSystem = () => ({
18 | type: AccountActionTypes.GET_FILESYSTEM,
19 | });
20 |
21 | export const createNewDirectoryItem = (item) => ({
22 | type: AccountActionTypes.CREATE_NEW_DIR_ITEM,
23 | payload: item,
24 | });
25 |
26 | export const renameDirectoryItems = (ids, newName) => ({
27 | type: AccountActionTypes.RENAME_DIR_ITEM,
28 | payload: { items: ids, newName },
29 | });
30 |
31 | export const unlinkDirectoryItems = (path, ids) => ({
32 | type: AccountActionTypes.DELETE_DIR_ITEM,
33 | payload: { ids, path },
34 | });
35 |
36 | export const copyDirectoryItems = (toPath, itemIds) => ({
37 | type: AccountActionTypes.COPY_DIR_ITEM,
38 | payload: { toPath, ids: itemIds },
39 | });
40 |
41 | export const moveDirectoryItems = (toPath, itemIds) => ({
42 | type: AccountActionTypes.MOVE_DIR_ITEM,
43 | payload: { toPath, ids: itemIds },
44 | });
45 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "jest": true
6 | },
7 | "extends": [
8 | "plugin:react/recommended",
9 | "airbnb",
10 | "prettier"
11 | ],
12 | "parser": "@babel/eslint-parser",
13 | "parserOptions": {
14 | "ecmaFeatures": {
15 | "jsx": true
16 | },
17 | "requireConfigFile": false,
18 | "ecmaVersion": 8,
19 | "sourceType": "module"
20 | },
21 | "plugins": [
22 | "react",
23 | "prettier"
24 | ],
25 | "rules": {
26 | "no-console": "warn",
27 | "react/react-in-jsx-scope": "off",
28 | "react/prop-types": "off",
29 | "no-unused-vars": "warn",
30 | "no-nested-ternary": "off",
31 | "import/prefer-default-export": "warn",
32 | "react/no-array-index-key": "warn",
33 | "react/jsx-props-no-spreading": [1, { "exceptions": ["button"] }],
34 | "jsx-a11y/no-static-element-interactions": "off",
35 | "jsx-a11y/interactive-supports-focus": "off",
36 | "jsx-a11y/click-events-have-key-events": "off",
37 | "jsx-a11y/no-noninteractive-element-interactions": "off",
38 | "prettier/prettier": "error",
39 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }]
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/MenuBar/MenuBar.jsx:
--------------------------------------------------------------------------------
1 | import { VscCheck } from 'react-icons/vsc';
2 | import './MenuBar.scss';
3 |
4 | const MenuBar = ({ items }) => {
5 | const MenubarSubmenu = ({ submenu }) =>
6 | submenu.map((subitem, j) => (
7 |
12 |
13 | {subitem.toggleValue && }
14 |
15 | {subitem.title}
16 |
17 | ));
18 |
19 | const MenubarItem = ({ item }) => (
20 |
26 | {item.title}
27 | {item.submenu && (
28 |
e.currentTarget.parentNode.blur()}
31 | >
32 |
33 |
34 | )}
35 |
36 | );
37 |
38 | return (
39 |
40 | {items.map((item, i) => (
41 |
42 | ))}
43 |
44 | );
45 | };
46 |
47 | export default MenuBar;
48 |
--------------------------------------------------------------------------------
/src/redux/auth/index.js:
--------------------------------------------------------------------------------
1 | import AuthActionTypes from './auth.types';
2 | import { checkForStoredUser, saveNewUser, safeguardUser, signInUser } from './auth.utils';
3 | import Strings from '../../config/strings';
4 |
5 | const initialState = {
6 | users: null,
7 | activeUser: null,
8 | success: false,
9 | error: null,
10 | };
11 |
12 | const AuthReducer = (state = initialState, action) => {
13 | switch (action.type) {
14 | case AuthActionTypes.CHECK_USER_SESSION: {
15 | const users = state.users || checkForStoredUser();
16 | return { ...state, users: safeguardUser(users) };
17 | }
18 |
19 | case AuthActionTypes.CREATE_NEW_ACCOUNT: {
20 | const newUsers = saveNewUser(action.payload);
21 | return { ...state, users: safeguardUser(newUsers) };
22 | }
23 |
24 | case AuthActionTypes.SIGN_IN: {
25 | const { userIndex, password } = action.payload;
26 | const signInSuccess = signInUser(userIndex, password);
27 | if (signInSuccess) {
28 | return { ...state, activeUser: state.users[userIndex], success: true };
29 | }
30 | return { ...state, error: { text: Strings.INCORRECT_PASSWORD, __id: new Date().getTime() } };
31 | }
32 |
33 | case AuthActionTypes.LOGOUT_USER: {
34 | return { ...state, success: null, error: null, activeUser: null };
35 | }
36 |
37 | default:
38 | return { ...state, users: safeguardUser(state.users) };
39 | }
40 | };
41 |
42 | export default AuthReducer;
43 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 |
28 | Mindows
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/config/apps.js:
--------------------------------------------------------------------------------
1 | import { FcCalculator, FcCalendar, FcDocument, FcFolder } from 'react-icons/fc';
2 | import { VscSettingsGear } from 'react-icons/vsc';
3 | import Calculator from '../softwares/Calculator/Calculator';
4 | import Calender from '../softwares/Calender/Calender';
5 | import Explorer from '../softwares/Explorer/Explorer';
6 | import Notepad from '../softwares/Notepad/Notepad';
7 | import Settings from '../softwares/Settings/Settings';
8 |
9 | const InstalledApps = {
10 | calculator: {
11 | id: 'calculator',
12 | name: 'Calculator',
13 | icon: FcCalculator,
14 | component: Calculator,
15 | config: {
16 | initWindowWidth: '320px',
17 | initWindowHeight: '480px',
18 | },
19 | },
20 | calendar: {
21 | id: 'calendar',
22 | name: 'Calendar',
23 | icon: FcCalendar,
24 | component: Calender,
25 | },
26 | fsexplorer: {
27 | id: 'fsexplorer',
28 | name: 'File Explorer',
29 | icon: FcFolder,
30 | component: Explorer,
31 | perms: {
32 | OPEN_DOCUMENT: true,
33 | },
34 | },
35 | notepad: {
36 | id: 'notepad',
37 | name: 'Notepad',
38 | icon: FcDocument,
39 | component: Notepad,
40 | config: {
41 | initTitle: 'Untitled - Notepad',
42 | initWindowWidth: '480px',
43 | initWindowHeight: '480px',
44 | },
45 | },
46 | settings: {
47 | id: 'settings',
48 | name: 'Settings',
49 | icon: VscSettingsGear,
50 | component: Settings,
51 | },
52 | };
53 |
54 | export default InstalledApps;
55 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mindows",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@babel/eslint-parser": "^7.15.0",
7 | "@testing-library/jest-dom": "^5.14.1",
8 | "@testing-library/react": "^11.2.7",
9 | "@testing-library/user-event": "^12.8.3",
10 | "eslint": "6.8.0",
11 | "eslint-config-airbnb": "^18.2.1",
12 | "eslint-config-prettier": "^8.3.0",
13 | "eslint-plugin-import": "2.22.1",
14 | "eslint-plugin-jsx-a11y": "6.4.1",
15 | "eslint-plugin-prettier": "^3.4.0",
16 | "eslint-plugin-react": "7.21.5",
17 | "eslint-plugin-react-hooks": "4",
18 | "prettier": "^2.3.2",
19 | "react": "^17.0.2",
20 | "react-dom": "^17.0.2",
21 | "react-draggable": "^4.4.3",
22 | "react-icons": "^4.2.0",
23 | "react-redux": "^7.2.4",
24 | "react-router-dom": "^5.2.0",
25 | "react-scripts": "4.0.3",
26 | "redux": "^4.1.0",
27 | "sass": "^1.35.2",
28 | "web-vitals": "^1.1.2"
29 | },
30 | "scripts": {
31 | "start": "react-scripts start",
32 | "build": "react-scripts build",
33 | "test": "react-scripts test",
34 | "eject": "react-scripts eject"
35 | },
36 | "eslintConfig": {
37 | "extends": [
38 | "react-app",
39 | "react-app/jest"
40 | ]
41 | },
42 | "browserslist": {
43 | "production": [
44 | ">0.2%",
45 | "not dead",
46 | "not op_mini all"
47 | ],
48 | "development": [
49 | "last 1 chrome version",
50 | "last 1 firefox version",
51 | "last 1 safari version"
52 | ]
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/softwares/Explorer/components/Ribbon/Ribbon.scss:
--------------------------------------------------------------------------------
1 | .disabled {
2 | content: "";
3 | position: absolute;
4 | top: 0;
5 | left: 0;
6 | bottom: 0;
7 | right: 0;
8 | background: rgba(233,236,238,.3);
9 | }
10 |
11 | .Explorer-ribbon {
12 | display: flex;
13 | width: 100%;
14 | align-content: center;
15 | background: #e9ecee;
16 | padding: 4px 0;
17 | border-top: 1px solid lightgray;
18 | border-bottom: 1px solid lightgray;
19 | cursor: default;
20 | position: relative;
21 |
22 | &-category {
23 | display: flex;
24 | flex-direction: column;
25 | border-right: 1px solid lightgray;
26 | position: relative;
27 |
28 | &-buttons {
29 | display: flex;
30 | padding: 0 4px;
31 | }
32 |
33 | &-label {
34 | text-align: center;
35 | color: rgba(0,0,0,0.8);
36 | }
37 |
38 | &-item {
39 | width: 56px;
40 | padding: 0 16px;
41 | height: 7em;
42 | display: flex;
43 | flex-direction: column;
44 | align-items: center;
45 | text-align: center;
46 | position: relative;
47 |
48 | &-icon {
49 | font-size: 2em;
50 |
51 | & > * {
52 | height: 1.5em;
53 | }
54 | }
55 | }
56 |
57 | &-item[disabled=""]::after {
58 | @extend .disabled;
59 | }
60 |
61 | &-item:not([disabled=""]):hover {
62 | background: lightblue;
63 | }
64 | }
65 |
66 | &-category[disabled=""]::after {
67 | @extend .disabled;
68 | }
69 | }
70 |
71 | .Explorer-ribbon[disabled=""]::after {
72 | @extend .disabled;
73 | }
--------------------------------------------------------------------------------
/src/pages/NewAccount/ChildView.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/mixins';
2 |
3 | .ChildView {
4 | @include mixins.fullscreen;
5 | position: absolute;
6 | top: 0;
7 | left: 0;
8 | display: flex;
9 | }
10 |
11 | .AccountNameView, .AccountPasswordView {
12 | @extend .ChildView;
13 | justify-content: space-between;
14 | align-items: center;
15 | flex-direction: column;
16 | transition: opacity 0.25s ease-out;
17 | }
18 |
19 | .AccountSettingUpView {
20 | @extend .ChildView;
21 | justify-content: center;
22 | align-items: center;
23 | flex-direction: column;
24 | }
25 |
26 | .heading {
27 |
28 | text-align: center;
29 | padding-top: 48px;
30 |
31 | p {
32 | color: white;
33 | @include mixins.fonttype('light');
34 | }
35 |
36 | &-title {
37 | font-size: 64px;
38 | }
39 |
40 | &-subtitle {
41 | font-size: 24px;
42 | }
43 | }
44 |
45 | .view-form {
46 |
47 | display: flex;
48 | flex-direction: column;
49 | align-items: center;
50 | margin-top: -24px;
51 |
52 | input {
53 | font-size: 18px;
54 | padding: 8px 8px;
55 | outline: none;
56 | border: 2px solid white;
57 | width: 480px;
58 | }
59 | }
60 |
61 | .view-form-pfp {
62 | border-radius: 50%;
63 | border: 2px solid white;
64 | font-size: 120px;
65 | height: 160px;
66 | width: 160px;
67 | display: flex;
68 | text-align: center;
69 | color: white;
70 | margin-bottom: 32px;
71 |
72 | > * {
73 | margin: auto;
74 | }
75 | }
76 |
77 | .view-footer {
78 |
79 | @include mixins.fonttype;
80 | width: 100%;
81 | padding: 0 32px 32px 32px;
82 | display: flex;
83 | flex-direction: row;
84 | justify-content: space-between;
85 | align-items: center;
86 |
87 | a {
88 | text-decoration: none;
89 | color: white;
90 | }
91 | }
--------------------------------------------------------------------------------
/src/softwares/Notepad/Notepad.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react';
2 | import './Notepad.scss';
3 |
4 | const STORENAME = 'documents';
5 | const formMenubarItems = (onSave, onExit, wordWrap, onWordWrap) => [
6 | {
7 | title: 'File',
8 | submenu: [
9 | { title: 'Save', action: onSave },
10 | { title: 'Exit', action: onExit },
11 | ],
12 | },
13 | {
14 | title: 'View',
15 | submenu: [{ title: 'Word Wrap', toggleValue: wordWrap, action: onWordWrap }],
16 | },
17 | ];
18 |
19 | const Notepad = (props) => {
20 | const { docId, onTerminate, updateProgramData, getProgramData, createMenubar } = props;
21 | const textRef = useRef(null);
22 | const [text, updateText] = useState('');
23 | const onExit = onTerminate;
24 | const [wordWrap, onToggleWordWrap] = useState(false);
25 | const style = { whiteSpace: wordWrap ? 'nowrap' : 'pre-wrap' };
26 |
27 | const onSave = () => {
28 | const data = {
29 | id: docId,
30 | content: textRef.current.value,
31 | };
32 | updateProgramData(STORENAME, data);
33 | };
34 |
35 | // get text from storage
36 | useEffect(() => {
37 | if (docId) {
38 | getProgramData(STORENAME, docId, (res) => updateText(res.data?.content || ''));
39 | }
40 | }, []);
41 |
42 | const menu = createMenubar(
43 | formMenubarItems(onSave, onExit, wordWrap, () => onToggleWordWrap(!wordWrap))
44 | );
45 |
46 | return (
47 |
48 | {menu}
49 |
50 |
56 |
57 |
58 | );
59 | };
60 |
61 | export default Notepad;
62 |
--------------------------------------------------------------------------------
/src/softwares/Explorer/components/Sidebar/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { MdDesktopWindows } from 'react-icons/md';
3 | import { FiHardDrive } from 'react-icons/fi';
4 | import Strings from '../../../../config/strings';
5 | import './Sidebar.scss';
6 |
7 | const Sidebar = React.memo(
8 | ({ selectedItem, rootDirectoryData, onSelectItem, onGoToDirectory }) => {
9 | const [directoryData, updateDirectoryData] = useState([]);
10 | useEffect(() => updateDirectoryData(rootDirectoryData), []);
11 |
12 | const SideNavbarItem = ({ item }) => (
13 | onGoToDirectory(item.id) || onSelectItem(item.id)}
18 | >
19 |
20 |
21 |
22 | {item.name}
23 |
24 | );
25 |
26 | return (
27 |
28 |
onGoToDirectory('_root') || onSelectItem('_root')}
33 | >
34 |
35 |
36 |
37 | {Strings.PC_FILESYSTEM_ROOT_NAME}
38 |
39 |
40 | {directoryData.map((item) => (
41 |
42 | ))}
43 |
44 |
45 | );
46 | },
47 | (prevProps, nextProps) => prevProps.selectedItem === nextProps.selectedItem
48 | );
49 |
50 | export default Sidebar;
51 |
--------------------------------------------------------------------------------
/src/components/MenuBar/MenuBar.scss:
--------------------------------------------------------------------------------
1 | @import '../../scss/variables';
2 |
3 | .Program {
4 | &-menubar {
5 | font-size: 13px;
6 | display: flex;
7 | align-items: center;
8 | justify-content: flex-start;
9 | flex-direction: row;
10 | padding: 0 2px 2px 2px;
11 | border-bottom: 2px solid $menuBarBorder;
12 |
13 | &-item {
14 | position: relative;
15 | padding: 4px 8px;
16 | border: 1px solid transparent;
17 |
18 | &-submenu {
19 | position: absolute;
20 | border: 1px solid darkgray;
21 | background: white;
22 | top: 100%;
23 | left: -1px;
24 | padding: 2px;
25 | min-width: 128px;
26 | width: max-content;
27 | display: none;
28 | box-shadow: 4px 4px 0 -2px #969696;
29 |
30 | &-item {
31 | padding: 0 16px 0 16px;
32 | border-radius: 2px;
33 | display: flex;
34 | align-items: center;
35 | border: 2px solid transparent;
36 |
37 | span {
38 | display: inline-block;
39 | padding: 4px 8px;
40 | }
41 |
42 | span:first-of-type {
43 | height: 16px;
44 | width: 16px;
45 | padding: 0;
46 | font-size: 1em;
47 |
48 | svg {
49 | stroke-width: 1.5;
50 | }
51 | }
52 | }
53 |
54 | &-item:hover {
55 | background: $itemBorderColor;
56 | }
57 | }
58 | }
59 |
60 | &-item:hover {
61 | background: $itemHoverBg;
62 | border: 1px solid $itemBorderColor;
63 | }
64 |
65 | &-item:focus {
66 | background: $itemFocusBg;
67 | border: 1px solid $itemBorderColor;
68 | }
69 |
70 | &-item:focus > &-item-submenu {
71 | display: block;
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/src/components/LockCover/LockCover.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import {
3 | subscribeClock,
4 | unsubscribeClock,
5 | formatDateTime,
6 | addKeyListener,
7 | removeKeyListener,
8 | } from '../../app.util';
9 | import './LockCover.scss';
10 |
11 | const dateOptions = { weekday: 'long', month: 'long', day: 'numeric' };
12 | const timeOptions = { hour12: true, hour: 'numeric', minute: '2-digit' };
13 |
14 | const LockCover = ({ background, onDoneLoading, autoHide }) => {
15 | const [classes, updateClass] = useState(`LockCover${autoHide ? ' LockCoverHidden' : ''}`);
16 | const [opacity, updateOpacity] = useState(0);
17 | const styles = {
18 | backgroundImage: `url(${process.env.PUBLIC_URL}"/images/${background}")`,
19 | };
20 | const [currentDateTime, updateCurrentDateTime] = useState(new Date());
21 | const dateTimeFormat = formatDateTime(currentDateTime, dateOptions, timeOptions);
22 |
23 | // add escape key event listener
24 | const onPressEscape = (event) => {
25 | if (event.key === 'Escape') updateClass('LockCover');
26 | };
27 |
28 | useEffect(() => {
29 | addKeyListener(onPressEscape);
30 | setTimeout(() => updateOpacity(1), 1500);
31 | return () => removeKeyListener(onPressEscape);
32 | }, []);
33 |
34 | // set time interval for current time clock
35 | useEffect(() => {
36 | const clockRef = subscribeClock(updateCurrentDateTime);
37 | return () => unsubscribeClock(clockRef);
38 | }, []);
39 |
40 | useEffect(() => opacity && onDoneLoading(true), [opacity]);
41 |
42 | return (
43 | updateClass('LockCover LockCoverHidden')}
47 | >
48 |
49 |
{dateTimeFormat.time}
50 |
{dateTimeFormat.date}
51 |
52 |
53 | );
54 | };
55 |
56 | export default LockCover;
57 |
--------------------------------------------------------------------------------
/src/redux/account/account.utils.js:
--------------------------------------------------------------------------------
1 | import { getUsername } from '../auth/auth.user';
2 |
3 | const storageKeys = {
4 | ACCOUNT_DATA: (username) => `ACCOUNT_DATA__${username}`,
5 | };
6 |
7 | export const getAccountFromStorage = (user) => {
8 | const username = getUsername(user);
9 | const data = localStorage.getItem(storageKeys.ACCOUNT_DATA(username));
10 | return data ? JSON.parse(data) : {};
11 | };
12 |
13 | export const saveAccountInStorage = (user, state) => {
14 | const username = getUsername(user);
15 | localStorage.setItem(storageKeys.ACCOUNT_DATA(username), JSON.stringify(state));
16 | };
17 |
18 | export const linkNodes = (fs, parentNode, childNodes) => {
19 | const newFs = { ...fs };
20 | const newChildren = [...parentNode.children].concat(childNodes.map((node) => node.node.id));
21 | newFs[parentNode.node.id] = { ...parentNode, children: newChildren };
22 | childNodes.forEach((node) => {
23 | newFs[node.node.id] = node;
24 | });
25 | return newFs;
26 | };
27 |
28 | export const renameNodes = (fs, ids, newName) => {
29 | const newFs = { ...fs };
30 | ids.forEach((id) => {
31 | newFs[id].node.name = newName;
32 | });
33 | return newFs;
34 | };
35 |
36 | export const unlinkNodes = (fs, parentNode, ids) => {
37 | const newFs = { ...fs };
38 | newFs[parentNode.node.id] = {
39 | ...parentNode,
40 | children: parentNode.children.filter((cId) => !ids.includes(cId)),
41 | };
42 | ids.forEach((id) => delete newFs[id]);
43 | return newFs;
44 | };
45 |
46 | export const copyNodes = (fs, childNodes, toParentNode) => linkNodes(fs, toParentNode, childNodes);
47 |
48 | export const moveNodes = (fs, childNodes, fromParentNode, toParentNode) => {
49 | if (fromParentNode.node.id === toParentNode.node.id) {
50 | return { ...fs };
51 | }
52 | return linkNodes(
53 | unlinkNodes(
54 | fs,
55 | fromParentNode,
56 | childNodes.map((node) => node.node.id)
57 | ),
58 | toParentNode,
59 | childNodes
60 | );
61 | };
62 |
--------------------------------------------------------------------------------
/src/components/BootLogo/BootLogo.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * The following code is taken from codepen created by Fernando de Almeida Faria.
3 | * https://codepen.io/feebaa/pen/PPrLQP
4 | */
5 |
6 | .loader {
7 | background: transparent;
8 | position: relative;
9 | }
10 |
11 | .loader .circle {
12 | position: absolute;
13 | opacity: 0;
14 | top: 20%;
15 | transform: rotate(225deg);
16 | animation-iteration-count: infinite;
17 | animation-name: orbit;
18 | animation-duration: 5.5s;
19 | }
20 |
21 | .loader .circle:after {
22 | content: '';
23 | position: absolute;
24 | width: 6px;
25 | height: 6px;
26 | border-radius: 5px;
27 | background: #fff;
28 | }
29 |
30 | .loader .circle:nth-child(2) {
31 | animation-delay: 240ms;
32 | }
33 |
34 | .loader .circle:nth-child(3) {
35 | animation-delay: 480ms;
36 | }
37 |
38 | .loader .circle:nth-child(4) {
39 | animation-delay: 720ms;
40 | }
41 |
42 | .loader .circle:nth-child(5) {
43 | animation-delay: 960ms;
44 | }
45 |
46 | .loader .bg {
47 | position: absolute;
48 | width: 70px;
49 | height: 70px;
50 | margin-left: -16px;
51 | margin-top: -16px;
52 | border-radius: 13px;
53 | background-color: transparent;
54 | animation: bgg 16087ms ease-in alternate infinite;
55 | }
56 |
57 | @keyframes orbit {
58 | 0% {
59 | transform: rotate(225deg);
60 | opacity: 1;
61 | animation-timing-function: ease-out;
62 | }
63 | 7% {
64 | transform: rotate(345deg);
65 | animation-timing-function: linear;
66 | }
67 | 30% {
68 | transform: rotate(455deg);
69 | animation-timing-function: ease-in-out;
70 | }
71 | 39% {
72 | transform: rotate(690deg);
73 | animation-timing-function: linear;
74 | }
75 | 70% {
76 | transform: rotate(815deg);
77 | opacity: 1;
78 | animation-timing-function: ease-out;
79 | }
80 | 75% {
81 | transform: rotate(945deg);
82 | animation-timing-function: ease-out;
83 | }
84 | 76% {
85 | transform: rotate(945deg);
86 | opacity: 0;
87 | }
88 | 100% {
89 | transform: rotate(945deg);
90 | opacity: 0;
91 | }
92 | }
--------------------------------------------------------------------------------
/src/pages/SwitchUser/SwitchUser.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { useHistory, useLocation } from 'react-router-dom';
4 |
5 | import LockCover from '../../components/LockCover/LockCover';
6 | import { selectBootBackgrounds } from '../../redux/boot/boot.selectors';
7 | import { signInUser } from '../../redux/auth/auth.actions';
8 | import { selectAuthError, selectAuthSuccess } from '../../redux/auth/auth.selectors';
9 | import { getBootBackgrounds } from '../../redux/boot/boot.actions';
10 | import LoginView from './LoginView';
11 | import './SwitchUser.scss';
12 |
13 | const SwitchUser = ({ users }) => {
14 | const backgrounds = useSelector(selectBootBackgrounds);
15 | const authError = useSelector(selectAuthError);
16 | const authSuccess = useSelector(selectAuthSuccess);
17 | const [hasDoneLoading, onDoneLoading] = useState(false);
18 | const dispatch = useDispatch();
19 | const location = useLocation();
20 | const history = useHistory();
21 |
22 | useEffect(() => dispatch(getBootBackgrounds()), [dispatch]);
23 | useEffect(() => {
24 | if (authSuccess) {
25 | setTimeout(() => {
26 | history.push('/desktop');
27 | }, 2000);
28 | }
29 | }, [authSuccess]);
30 |
31 | const onLogin = (userIndex, password) => {
32 | dispatch(signInUser(userIndex, password));
33 | };
34 |
35 | return (
36 | <>
37 | {hasDoneLoading && (
38 |
45 | )}
46 | {backgrounds.lock && backgrounds.login && (
47 | setTimeout(() => onDoneLoading(to), 1000)}
51 | />
52 | )}
53 | >
54 | );
55 | };
56 |
57 | export default SwitchUser;
58 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { Switch, Route, useLocation, Redirect } from 'react-router-dom';
4 |
5 | import { selectActiveUser, selectUsers } from './redux/auth/auth.selectors';
6 | import { checkUserSession } from './redux/auth/auth.actions';
7 | import SwitchUser from './pages/SwitchUser/SwitchUser';
8 | import BootScreen from './pages/BootScreen/BootScreen';
9 | import NewAccount from './pages/NewAccount/NewAccount';
10 | import './App.scss';
11 | import Desktop from './pages/Desktop/Desktop';
12 |
13 | const App = () => {
14 | const users = useSelector(selectUsers);
15 | const activeUser = useSelector(selectActiveUser);
16 | const dispatch = useDispatch();
17 | const location = useLocation();
18 | useEffect(() => setTimeout(() => dispatch(checkUserSession()), 2000), [dispatch]);
19 |
20 | return (
21 |
22 |
23 |
27 | users ? (
28 | users.length ? (
29 |
30 | ) : (
31 |
32 | )
33 | ) : (
34 |
35 | )
36 | }
37 | />
38 |
39 |
42 | users ? (
43 | users.length ? (
44 |
45 | ) : (
46 |
47 | )
48 | ) : (
49 |
50 | )
51 | }
52 | />
53 |
54 | (users ? : )} />
55 |
56 | (activeUser ? : )}
59 | />
60 |
61 |
62 | );
63 | };
64 |
65 | export default App;
66 |
--------------------------------------------------------------------------------
/src/pages/NewAccount/ChildView.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { AiOutlineUser } from 'react-icons/ai';
4 |
5 | import BootLogo from '../../components/BootLogo/BootLogo';
6 | import Button from '../../components/Button/Button';
7 | import Strings from '../../config/strings';
8 | import './ChildView.scss';
9 |
10 | const ChildView = (props) => {
11 | const { childClass, onFieldUpdate, value, signInOpt, onSubmit, details, validity } = props;
12 | const [opacity, updateOpacity] = useState(0);
13 | useEffect(() => updateOpacity(1), []);
14 | useEffect(() => updateOpacity(1), [childClass]);
15 |
16 | return (
17 |
18 | {childClass === 'AccountSettingUpView' ? (
19 | <>
20 |
21 |
22 |
Just a moment...
23 |
24 | >
25 | ) : (
26 | <>
27 |
28 |
{details.title}
29 |
{details.subtitle}
30 |
31 |
32 |
33 |
36 |
37 | onFieldUpdate(e.target.value)}
43 | />
44 |
45 |
46 |
47 |
48 |
49 | {signInOpt && (
50 | {Strings.LOGIN_LINK_SIGNUP_VIEW}
51 | )}
52 |
53 |
54 |
61 |
62 | >
63 | )}
64 |
65 | );
66 | };
67 |
68 | export default ChildView;
69 |
--------------------------------------------------------------------------------
/src/softwares/Explorer/Explorer.scss:
--------------------------------------------------------------------------------
1 | $fontsize: 12px;
2 | $diritemSize: 2em;
3 | @import '../../scss/variables';
4 |
5 | .Explorer {
6 | width: 100%;
7 | height: 100%;
8 | font-size: $fontsize;
9 | position: relative;
10 | display: flex;
11 | flex-direction: column;
12 | }
13 |
14 | .Explorer-viewport {
15 | display: flex;
16 | flex-direction: row;
17 | flex-grow: 1;
18 | flex-shrink: 1;
19 | flex-basis: 0;
20 | overflow-y: auto;
21 |
22 | > * {
23 | padding: 8px 0;
24 | }
25 | }
26 |
27 | .Explorer-fs {
28 | flex: 1;
29 | padding-left: 1em;
30 | padding-right: 1em;
31 |
32 | &-item {
33 | display: flex;
34 | flex-direction: column;
35 | align-items: center;
36 | justify-content: center;
37 | padding: 4px 8px;
38 | margin-right: 8px;
39 | margin-bottom: 4px;
40 | cursor: default;
41 | width: 7.2em;
42 | text-align: center;
43 | position: relative;
44 | border: 2px solid transparent;
45 | border-radius: 2px;
46 |
47 | & span:first-of-type {
48 | font-size: $diritemSize;
49 |
50 | img {
51 | height: $diritemSize * 1.5;
52 | }
53 |
54 | svg {
55 | font-size: $diritemSize;
56 | }
57 | }
58 |
59 | &-hidden {
60 | display: none;
61 | }
62 |
63 | &-pseudo-name {
64 | font-size: $fontsize;
65 | background: transparent;
66 | text-align: center;
67 | width: 100%;
68 | padding: 0;
69 | outline: none;
70 | border: 0;
71 | }
72 | }
73 |
74 | &-item:hover {
75 | background: $itemHoverBg;
76 | border: 2px solid $itemBorderColor;
77 | }
78 |
79 | &-item-selected {
80 | border: 2px solid $itemBorderColor;
81 | background: $itemFocusBg;
82 | }
83 |
84 | &-item-cut::after {
85 | content: '';
86 | position: absolute;
87 | top: 0;
88 | bottom: 0;
89 | left: 0;
90 | right: 0;
91 | background: rgba(255,255,255,0.4);
92 | }
93 | }
94 |
95 | .Explorer-directory {
96 | display: flex;
97 | align-items: flex-start;
98 | flex-wrap: wrap;
99 | }
100 |
101 | .Explorer-rootdirectory {
102 | @extend .Explorer-directory;
103 |
104 | & .Explorer-fs-item {
105 | flex-direction: row;
106 | width: 16em;
107 |
108 | & span:first-of-type {
109 | margin-right: 16px;
110 | }
111 | }
112 | }
113 |
114 | .Explorer-item-selected {
115 | background: aqua;
116 | }
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/StartMenu/Promotions.jsx:
--------------------------------------------------------------------------------
1 | import { FcBearish, FcMusic, FcCamera, FcGoogle, FcNews, FcPicture, FcSteam } from 'react-icons/fc';
2 | import {
3 | FaDropbox,
4 | FaAmazon,
5 | FaChrome,
6 | FaFacebookF,
7 | FaFirefoxBrowser,
8 | FaGithub,
9 | FaMicrosoft,
10 | FaSkype,
11 | FaXbox,
12 | } from 'react-icons/fa';
13 | import { MdMovieCreation } from 'react-icons/md';
14 | import { WiDaySunny } from 'react-icons/wi';
15 | import { SiMicrosoftword } from 'react-icons/si';
16 |
17 | const Promotions = [
18 | {
19 | icon: FcGoogle,
20 | text: 'Google Search',
21 | action: 'https://www.google.com/',
22 | },
23 | {
24 | icon: FaAmazon,
25 | text: 'Amazon',
26 | action: 'https://www.amazon.com/',
27 | },
28 | {
29 | icon: FaChrome,
30 | text: 'Google Chrome',
31 | action: 'https://www.google.com/intl/en_uk/chrome/',
32 | },
33 | {
34 | icon: FaDropbox,
35 | text: 'Dropbox',
36 | action: 'https://www.dropbox.com/',
37 | },
38 | {
39 | icon: FaFacebookF,
40 | text: 'Facebook',
41 | action: 'https://www.facebook.com/',
42 | },
43 | {
44 | icon: FaFirefoxBrowser,
45 | text: 'Firefox',
46 | action: 'https://www.mozilla.org/en-US/firefox/new/',
47 | },
48 | {
49 | icon: FaGithub,
50 | text: 'Mindows',
51 | action: 'https://www.github.com/piyush078/mindows',
52 | },
53 | {
54 | icon: FaSkype,
55 | text: 'Skype',
56 | action: 'https://www.skype.com/en/',
57 | },
58 | {
59 | icon: WiDaySunny,
60 | text: 'Weather',
61 | action: 'https://weather.com/',
62 | },
63 | {
64 | icon: SiMicrosoftword,
65 | text: 'Word',
66 | action: 'https://www.microsoft.com/en-in/microsoft-365/business',
67 | },
68 | {
69 | icon: FcSteam,
70 | text: 'Steam',
71 | action: 'https://store.steampowered.com/',
72 | },
73 | {
74 | icon: FcCamera,
75 | text: 'Camera',
76 | },
77 | {
78 | icon: FaMicrosoft,
79 | text: 'Windows 11',
80 | action: 'https://www.microsoft.com/en-in/windows/windows-11',
81 | },
82 | {
83 | icon: FcNews,
84 | text: 'News',
85 | action: 'https://www.bing.com/news',
86 | },
87 | {
88 | icon: MdMovieCreation,
89 | text: 'Movies',
90 | },
91 | {
92 | icon: FaXbox,
93 | text: 'Xbox',
94 | },
95 | {
96 | icon: FcMusic,
97 | text: 'Music',
98 | },
99 | {
100 | icon: FcBearish,
101 | text: 'Money',
102 | },
103 | {
104 | icon: FcPicture,
105 | text: 'Gallery',
106 | action: 'https://onedrive.live.com/about/en-us/signin/',
107 | },
108 | ];
109 |
110 | export default Promotions;
111 |
--------------------------------------------------------------------------------
/src/pages/NewAccount/NewAccount.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { useHistory } from 'react-router-dom';
4 | import { VscArrowLeft } from 'react-icons/vsc';
5 |
6 | import { getUsername, User } from '../../redux/auth/auth.user';
7 | import { selectUsers } from '../../redux/auth/auth.selectors';
8 | import { createNewAccount } from '../../redux/auth/auth.actions';
9 | import Strings from '../../config/strings';
10 | import ChildView from './ChildView';
11 | import './NewAccount.scss';
12 |
13 | const details = [
14 | {
15 | title: Strings.SIGNUP_NAME_VIEW_TITLE,
16 | subtitle: Strings.SIGNUP_NAME_VIEW_SUBTITLE,
17 | type: 'text',
18 | placeholder: 'User name',
19 | },
20 | {
21 | title: Strings.SIGNUP_PASSWORD_VIEW_TITLE,
22 | subtitle: Strings.SIGNUP_PASSWORD_VIEW_SUBTITLE,
23 | type: 'password',
24 | placeholder: 'Password',
25 | },
26 | ];
27 |
28 | const NewAccount = () => {
29 | const users = useSelector(selectUsers);
30 | const dispatch = useDispatch();
31 | const history = useHistory();
32 |
33 | const [profileName, updateName] = useState('');
34 | const [profilePassword, updatePassword] = useState('');
35 | const [currentView, updateCurrentView] = useState(true);
36 |
37 | useEffect(() => {
38 | if (currentView === -1) {
39 | dispatch(createNewAccount(User(profileName, profileName, profilePassword)));
40 | setTimeout(() => history.push('/switchuser?cover=false'), 2000);
41 | }
42 | }, [currentView]);
43 |
44 | return (
45 |
46 |
47 |
48 | {!currentView && updateCurrentView(!currentView)} />}
49 |
50 |
Account
51 |
52 |
53 |
54 |
55 | {currentView === -1 ? (
56 |
57 | ) : (
58 | 0 : false}
63 | onSubmit={() => (currentView ? updateCurrentView(!currentView) : updateCurrentView(-1))}
64 | details={details[+!currentView]}
65 | validity={(val) =>
66 | currentView ? users.filter((user) => getUsername(user) === val).length !== 0 : false
67 | }
68 | />
69 | )}
70 |
71 |
72 | );
73 | };
74 |
75 | export default NewAccount;
76 |
--------------------------------------------------------------------------------
/src/redux/account/index.js:
--------------------------------------------------------------------------------
1 | import AccountActionTypes from './account.types';
2 | import {
3 | renameNodes,
4 | getAccountFromStorage,
5 | unlinkNodes,
6 | linkNodes,
7 | copyNodes,
8 | moveNodes,
9 | saveAccountInStorage,
10 | } from './account.utils';
11 | import Node from './account.fs';
12 | import Wallpapers from '../../config/wallpapers';
13 |
14 | const initialState = {
15 | settings: {
16 | background: Wallpapers.list[Wallpapers.defaultWallpaper].name,
17 | },
18 | taskbarApps: ['fsexplorer', 'notepad'],
19 | filesystem: {
20 | _root: Node('', true, null, ['C:', 'D:'], null, '_root'),
21 | 'C:': Node('Local Drive (C:)', true, '_root', [], null, 'C:'),
22 | 'D:': Node('Local Drive (D:)', true, '_root', [], null, 'D:'),
23 | },
24 | defaultApps: {
25 | '*': 'notepad',
26 | },
27 | };
28 |
29 | const AccountReducer = (state = initialState, action) => {
30 | switch (action.type) {
31 | case AccountActionTypes.LOAD_ACCOUNT: {
32 | const loadedAccount = getAccountFromStorage(action.payload);
33 | return { ...state, ...loadedAccount };
34 | }
35 |
36 | case AccountActionTypes.SAVE_ACCOUNT: {
37 | const activeAccount = action.payload;
38 | saveAccountInStorage(activeAccount, state);
39 | return state;
40 | }
41 |
42 | case AccountActionTypes.CREATE_NEW_DIR_ITEM: {
43 | const item = action.payload;
44 | const newNode = Node(item.name, item.isDir, item.path);
45 | const newFs = linkNodes(state.filesystem, state.filesystem[item.path], [newNode]);
46 | return { ...state, filesystem: newFs };
47 | }
48 |
49 | case AccountActionTypes.RENAME_DIR_ITEM: {
50 | const itemsId = action.payload.items;
51 | const { newName } = action.payload;
52 | const newFs = renameNodes(state.filesystem, itemsId, newName);
53 | return { ...state, filesystem: newFs };
54 | }
55 |
56 | case AccountActionTypes.DELETE_DIR_ITEM: {
57 | const parent = action.payload.path;
58 | const idsToDelete = action.payload.ids;
59 | const newFs = unlinkNodes(state.filesystem, state.filesystem[parent], idsToDelete);
60 | return { ...state, filesystem: newFs };
61 | }
62 |
63 | case AccountActionTypes.COPY_DIR_ITEM: {
64 | const { toPath } = action.payload;
65 | const idsToCopy = action.payload.ids;
66 | const nodesToCopy = idsToCopy.map((id) => state.filesystem[id]);
67 | const newFs = copyNodes(
68 | state.filesystem,
69 | nodesToCopy.map((node) => Node(node.node.name, node.node.isDir, toPath)),
70 | state.filesystem[toPath]
71 | );
72 | return { ...state, filesystem: newFs };
73 | }
74 |
75 | case AccountActionTypes.MOVE_DIR_ITEM: {
76 | const { toPath } = action.payload;
77 | const idsToMove = action.payload.ids;
78 | const fromPath = state.filesystem[idsToMove[0]].parent;
79 | const nodesToMove = idsToMove.map((id) => state.filesystem[id]);
80 | const newFs = moveNodes(
81 | state.filesystem,
82 | nodesToMove,
83 | state.filesystem[fromPath],
84 | state.filesystem[toPath]
85 | );
86 | return { ...state, filesystem: newFs };
87 | }
88 |
89 | default:
90 | return state;
91 | }
92 | };
93 |
94 | export default AccountReducer;
95 |
--------------------------------------------------------------------------------
/src/softwares/Explorer/components/Directory/Directory.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react';
2 | import { FiHardDrive } from 'react-icons/fi';
3 | import Icon from '../Icon/Icon';
4 |
5 | const defaultName = (isDir) => (isDir === 'dir' ? 'New Folder' : 'New Document');
6 | const newItem = (itemType) => ({
7 | id: '_new',
8 | name: defaultName(itemType === 'dir'),
9 | isDir: itemType === 'dir',
10 | });
11 | const formatDirectoryItems = (items, newItemMode) =>
12 | items.concat(newItemMode ? newItem(newItemMode) : []);
13 |
14 | const Directory = ({
15 | items,
16 | clipboard,
17 | selectedItems,
18 | isRootDirectory,
19 | onDoubleClick,
20 | createMode,
21 | renameMode,
22 | onCreateItem,
23 | onRenameItem,
24 | onSelectItem,
25 | onCancelRename,
26 | onOpenDocument,
27 | }) => {
28 | const [newName, updateNewName] = useState('');
29 | const inputRef = useRef(null);
30 | const onDoneRename = (toName) => (createMode ? onCreateItem(toName) : onRenameItem(toName));
31 | const dirItems = formatDirectoryItems(items, createMode);
32 |
33 | // on change of createMode or renameMode, update input field with name
34 | useEffect(() => {
35 | if (createMode) {
36 | updateNewName(defaultName(createMode));
37 | inputRef.current.focus();
38 | } else if (renameMode) {
39 | updateNewName(dirItems.filter((item) => item.id === selectedItems[0])[0].name);
40 | inputRef.current.focus();
41 | }
42 | }, [createMode, renameMode]);
43 |
44 | return (
45 |
46 | {dirItems.map((item) => {
47 | const toBeRenamed = item.id === '_new' || (selectedItems.includes(item.id) && renameMode);
48 | const className =
49 | `Explorer-fs-item ` +
50 | `${selectedItems.includes(item.id) ? ' Explorer-fs-item-selected' : ''} ` +
51 | `${clipboard.includes(item.id) ? ' Explorer-fs-item-cut' : ''}`;
52 |
53 | return (
54 |
onSelectItem(item.id)}
60 | onDoubleClick={() =>
61 | item.isDir
62 | ? onDoubleClick(item.id)
63 | : onOpenDocument(item.id, item.name, item.extension)
64 | }
65 | >
66 | {isRootDirectory ? : }
67 |
68 | {item.name}
69 | {item.extension ? `.${item.extension}` : ''}
70 |
71 |
72 | {toBeRenamed && (
73 | updateNewName(e.target.value)}
79 | onKeyUp={(e) => e.key === 'Enter' && onDoneRename(newName)}
80 | onBlur={() => onCancelRename(item.name)}
81 | />
82 | )}
83 |
84 | );
85 | })}
86 |
87 | );
88 | };
89 |
90 | export default Directory;
91 |
--------------------------------------------------------------------------------
/src/components/TaskBar/TaskBar.jsx:
--------------------------------------------------------------------------------
1 | import { GrWindows } from 'react-icons/gr';
2 | import { VscClose, VscRss, VscSearch, VscTriangleUp, VscUnmute } from 'react-icons/vsc';
3 | import { FaBatteryFull, FaCommentAlt } from 'react-icons/fa';
4 |
5 | import Clock from '../Clock/Clock';
6 | import Strings from '../../config/strings';
7 | import './TaskBar.scss';
8 |
9 | const TaskBarAppInstances = ({
10 | apps,
11 | programs,
12 | data,
13 | onIconClick,
14 | onInstanceClick,
15 | onCloseInstance,
16 | }) => {
17 | const Instances = ({ instances }) =>
18 | instances.map((instance) => (
19 | onInstanceClick(instance)}
23 | >
24 | {data[instance].title}
25 | onCloseInstance(instance)}
28 | >
29 |
30 |
31 |
32 | ));
33 |
34 | const App = ({ app, onClick }) => {
35 | const instances = programs[app.id] || [];
36 | return (
37 | (instances.length ? true : onClick(app))}
41 | >
42 | {app.icon()}
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | return apps.map((app) => );
51 | };
52 |
53 | const TaskBar = ({
54 | apps,
55 | programs,
56 | programsData,
57 | onIconClick,
58 | onMindowsClick,
59 | onInstanceClick,
60 | onCloseInstance,
61 | }) => (
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | {Strings.TASKBAR_SEARCH_PLACEHOLDER}
71 |
72 |
73 |
74 |
75 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | );
107 |
108 | export default TaskBar;
109 |
--------------------------------------------------------------------------------
/src/components/TaskBar/TaskBar.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/mixins';
2 | @import '../../scss/variables';
3 |
4 | $taskBarHeight: 48px;
5 | $taskBarAppSpacing: 16px;
6 |
7 | .TaskBar-children {
8 | font-size: 16px;
9 | margin-bottom: -4px;
10 | display: flex;
11 | height: $taskBarHeight;
12 | align-items: center;
13 | }
14 |
15 | .TaskBar-left {
16 | display: flex;
17 | align-items: flex-start;
18 |
19 | > * {
20 | @extend .TaskBar-children;
21 | padding: 0 $taskBarAppSpacing;
22 | }
23 | }
24 |
25 | .TaskBar-right {
26 | display: flex;
27 | align-items: flex-end;
28 |
29 | > * {
30 | @extend .TaskBar-children;
31 | padding: 0 ($taskBarAppSpacing * .5);
32 | }
33 | }
34 |
35 | .TaskBar {
36 | position: absolute;
37 | bottom: 0;
38 | left: 0;
39 | width: 100%;
40 | height: $taskBarHeight !important;
41 | background: $taskBarBg;
42 | display: flex;
43 | align-items: center;
44 | justify-content: space-between;
45 | z-index: 10000;
46 |
47 | > * {
48 | height: $taskBarHeight;
49 | }
50 |
51 | svg {
52 | fill: $taskBarFg;
53 | }
54 |
55 | &-searchbar {
56 | position: relative;
57 | width: 400px;
58 | height: $taskBarHeight;
59 |
60 | &-pseudo, input {
61 | width: 100%;
62 | position: absolute;
63 | top: 0;
64 | left: 0;
65 | bottom: 0;
66 | right: 0;
67 | font-size: 16px;
68 | padding: 0 $taskBarAppSpacing;
69 | }
70 |
71 | &-pseudo {
72 | display: flex;
73 | align-items: center;
74 | color: white;
75 | background: #625959;
76 |
77 | span {
78 | margin-left: 12px;
79 | }
80 | }
81 |
82 | input {
83 | outline: none;
84 | border: 0;
85 | height: $taskBarHeight;
86 | transition: background 0.25s ease;
87 | }
88 |
89 | input:not(:valid) {
90 | background: transparent;
91 | }
92 |
93 | input:valid, input:focus {
94 | background: white;
95 | color: black;
96 | }
97 | }
98 |
99 | &-app {
100 | margin-left: 4px;
101 | font-size: 24px;
102 |
103 | &-instances {
104 | position: absolute;
105 | font-size: 12px;
106 | display: none;
107 | flex-direction: column;
108 | width: max-content;
109 | min-width: 240px;
110 | bottom: 100%;
111 | top: -1;
112 | transform: translateX(-50%);
113 | background: $taskBarHoverBg;
114 |
115 | &-item {
116 | display: flex;
117 | align-items: center;
118 | justify-content: space-between;
119 | padding: 6px 16px;
120 | cursor: default;
121 | margin: 2px 8px;
122 | border-radius: 4px;
123 |
124 | &-title {
125 | @include mixins.fonttype('semibold');
126 | margin-right: 8px;
127 | color: white;
128 | }
129 |
130 | &-close {
131 | padding: 2px;
132 |
133 | > * {
134 | margin-bottom: -2px;
135 | }
136 | }
137 | }
138 |
139 | &-item:first-of-type {
140 | margin-top: 6px;
141 | }
142 |
143 | &-item:last-of-type {
144 | margin-bottom: 6px;
145 | }
146 |
147 | &-item:hover {
148 | background: rgba(227, 21, 62, 0.4);
149 | }
150 | }
151 | }
152 |
153 | &-app:hover > * {
154 | display: flex;
155 | }
156 |
157 | &-app-running {
158 | border-bottom: 4px solid aqua;
159 | background: rgba(192, 192, 192, 0.1);
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/pages/SwitchUser/LoginView.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/mixins';
2 | $pfpBackground: rgba(188, 188, 188, 0.5);
3 |
4 | .LoginScreen {
5 | @include mixins.fullscreen;
6 | @include mixins.wallpaperbg;
7 | }
8 |
9 | .LoginScreen::after {
10 | @include mixins.overlay;
11 | }
12 |
13 | .PseudoLoginScreen {
14 | @include mixins.fullscreen;
15 |
16 | display: flex;
17 | position: absolute;
18 | flex: 1;
19 | top: 0;
20 | left: 0;
21 |
22 | > * {
23 | margin: auto;
24 | }
25 | }
26 |
27 | .login-form {
28 |
29 | @include mixins.fonttype('light');
30 | $pfpWidth: 200px;
31 | display: flex;
32 | flex-direction: column;
33 | align-items: center;
34 |
35 | .login-pfp {
36 | height: $pfpWidth;
37 | width: $pfpWidth;
38 | border-radius: 50%;
39 | background: $pfpBackground;
40 | font-size: 160px;
41 | text-align: center;
42 | color: white;
43 |
44 | > * {
45 | margin: auto;
46 | }
47 | }
48 |
49 | .login-username {
50 | color: white;
51 | font-size: 48px;
52 | margin-top: 16px;
53 | }
54 |
55 | .login-fields {
56 |
57 | $inputBackground: rgba(14, 20, 32, 0.5);
58 | margin-top: 24px;
59 |
60 | input {
61 | background: $inputBackground;
62 | width: 320px;
63 | font-size: 18px;
64 | padding: 8px 8px;
65 | outline: none;
66 | border: 2px solid white;
67 | color: white;
68 | }
69 |
70 | .login-fields-username > input {
71 | margin-bottom: 16px;
72 | }
73 |
74 | input:focus, input:valid {
75 | background: white;
76 | color: black;
77 | border: 1px solid white;
78 | }
79 |
80 | input:not(:valid) + .login-fields-password-eye {
81 | visibility: hidden;
82 | }
83 |
84 | input:focus + .login-fields-password-eye svg, input:valid + .login-fields-password-eye svg {
85 | fill: black;
86 | }
87 |
88 | .login-fields-password {
89 | position: relative;
90 |
91 | input {
92 | padding-right: 80px;
93 | }
94 |
95 | .login-fields-password-svgs {
96 | position: absolute;
97 | display: flex;
98 | flex: 1;
99 | top: 1px;
100 | bottom: 1px;
101 | width: 36px;
102 | cursor: pointer;
103 |
104 | > * {
105 | margin: auto;
106 | fill: white;
107 | }
108 | }
109 |
110 | .login-fields-password-arrow {
111 | @extend .login-fields-password-svgs;
112 | right: 1px;
113 | background: rgb(128,128,128);
114 | }
115 |
116 | .login-fields-password-eye {
117 | @extend .login-fields-password-svgs;
118 | right: 40px;
119 | }
120 | }
121 | }
122 | }
123 |
124 | .login-users-list {
125 | position: absolute;
126 | bottom: 32px;
127 | left: 32px;
128 | cursor: default;
129 |
130 | &-item {
131 | @include mixins.fonttype;
132 | display: flex;
133 | align-items: center;
134 | color: white;
135 | font-size: 16px;
136 | padding: 4px 8px;
137 |
138 | &-pfp {
139 | display: flex;
140 | background: $pfpBackground;
141 | height: 48px;
142 | width: 48px;
143 | border-radius: 50%;
144 | margin-right: 16px;
145 | }
146 |
147 | & svg {
148 | height: 24px;
149 | width: 24px;
150 | margin: auto;
151 | }
152 | }
153 |
154 | li[current='1'] {
155 | background: #1c7797;
156 | }
157 | }
158 |
159 | .LoginSuccess {
160 | @include mixins.fonttype;
161 | display: flex;
162 | margin-top: 24px;
163 | font-size: 20px;
164 | color: white;
165 | align-items: center;
166 |
167 | span {
168 | margin-left: 8px;
169 | }
170 | }
171 |
172 | .LoginError {
173 | @extend .LoginSuccess;
174 | flex-direction: column;
175 | align-items: center;
176 |
177 | span {
178 | margin-left: 0;
179 | font-size: 18px;
180 | margin-bottom: 16px;
181 | }
182 |
183 | button {
184 | background: rgba(255,255,255,0.2);
185 | border: 2px solid white;
186 | }
187 | }
--------------------------------------------------------------------------------
/src/pages/SwitchUser/LoginView.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { VscEye, VscArrowRight } from 'react-icons/vsc';
3 | import { AiOutlineUser } from 'react-icons/ai';
4 | import { Link } from 'react-router-dom';
5 |
6 | import { getName, getUsername } from '../../redux/auth/auth.user';
7 | import Strings from '../../config/strings';
8 | import BootLogo from '../../components/BootLogo/BootLogo';
9 | import Button from '../../components/Button/Button';
10 | import './LoginView.scss';
11 |
12 | const LoginError = ({ error, onCancel }) => (
13 |
14 | {error.text}
15 |
16 |
17 | );
18 |
19 | const LoginSuccess = () => (
20 |
21 |
22 | {Strings.SUCCESSFUL_LOGIN_WELCOME_TEXT}
23 |
24 | );
25 |
26 | const LoginForm = ({ onSubmit }) => {
27 | const [isHiddenPassword, togglePasswordMode] = useState(true);
28 | const [password, updatePassword] = useState('');
29 |
30 | return (
31 |
32 | {false && (
33 |
34 |
35 |
36 | )}
37 |
38 |
39 |
updatePassword(e.target.value)}
43 | onKeyUp={(e) => e.key === 'Enter' && onSubmit(password)}
44 | type={isHiddenPassword ? 'password' : 'text'}
45 | placeholder="Password"
46 | />
47 |
48 |
togglePasswordMode(false)}
51 | onMouseUp={() => togglePasswordMode(true)}
52 | >
53 |
54 |
55 |
56 |
onSubmit(password)}>
57 |
58 |
59 |
60 |
61 | );
62 | };
63 |
64 | const LoginView = ({ users, background, onLogin, authError, authSuccess }) => {
65 | const [selectedUser, changeSelectedUser] = useState(0);
66 | const styles = {
67 | backgroundImage: `url(${process.env.PUBLIC_URL}"/images/${background}")`,
68 | };
69 | const [showError, toggleShowError] = useState(authError);
70 | useEffect(() => toggleShowError(authError), [authError]);
71 |
72 | return (
73 | <>
74 |
75 |
76 |
77 |
80 |
{users[selectedUser].name}
81 | {showError ? (
82 |
toggleShowError(false)} />
83 | ) : authSuccess ? (
84 |
85 | ) : (
86 | onLogin(selectedUser, password)} />
87 | )}
88 | {!authSuccess && (
89 |
90 | {Strings.SIGNUP_LINK_LOGIN_VIEW}
91 |
92 | )}
93 |
94 |
95 | {!authSuccess && users.length > 1 && (
96 |
97 | {users.map((user, ind) => (
98 |
changeSelectedUser(ind)}
103 | >
104 |
107 | {getName(user)}
108 |
109 | ))}
110 |
111 | )}
112 |
113 | >
114 | );
115 | };
116 |
117 | export default LoginView;
118 |
--------------------------------------------------------------------------------
/src/softwares/Explorer/Explorer.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 |
4 | import Ribbon from './components/Ribbon/Ribbon';
5 | import Sidebar from './components/Sidebar/Sidebar';
6 | import Directory from './components/Directory/Directory';
7 | import { selectDirectoryItems } from '../../redux/account/account.selectors';
8 | import './Explorer.scss';
9 | import {
10 | copyDirectoryItems,
11 | createNewDirectoryItem,
12 | moveDirectoryItems,
13 | renameDirectoryItems,
14 | unlinkDirectoryItems,
15 | } from '../../redux/account/account.actions';
16 |
17 | const rootDir = '_root';
18 |
19 | const Explorer = (props) => {
20 | const { onOpenDocument } = props;
21 | const dispatch = useDispatch();
22 | const [currentDir, updateCurrentDir] = useState(rootDir);
23 | const directoryData = useSelector(selectDirectoryItems(currentDir)) || [];
24 | const [selectedItems, updateSelected] = useState([]);
25 | const [chosenCategory, chooseCategory] = useState(rootDir);
26 | const [createMode, updateCreateMode] = useState(false);
27 | const [renameMode, updateRenameMode] = useState(false);
28 | const [clipboard, updateClipboard] = useState({ mode: -1, items: [] });
29 |
30 | const onGoToDirectory = (id) =>
31 | updateCurrentDir(id) || updateSelected([]) || (rootDir === currentDir && chooseCategory(id));
32 | const onSelectItem = (id) => updateSelected([id]);
33 | const onCancelRename = () => (renameMode ? updateRenameMode(false) : updateCreateMode(false));
34 |
35 | // delete selected items
36 | const onDeleteItem = () => {
37 | dispatch(unlinkDirectoryItems(currentDir, selectedItems));
38 | updateSelected([]);
39 | };
40 |
41 | // dispatch to create a new file/folder
42 | const onCreateItem = (name) => {
43 | const isDir = createMode === 'dir';
44 | updateCreateMode(false);
45 | dispatch(createNewDirectoryItem({ name, isDir, path: currentDir }));
46 | };
47 |
48 | // dispatch to rename a file/folder
49 | const onRenameItem = (newName) => {
50 | updateRenameMode(false);
51 | dispatch(renameDirectoryItems(selectedItems, newName));
52 | };
53 |
54 | // on paste the copy/cut items
55 | const onPasteItems = () => {
56 | if (clipboard.mode === 0) {
57 | dispatch(moveDirectoryItems(currentDir, clipboard.items));
58 | updateClipboard({ mode: -1, items: [] });
59 | } else {
60 | dispatch(copyDirectoryItems(currentDir, clipboard.items));
61 | }
62 | };
63 |
64 | return (
65 |
66 |
71 | updateCreateMode(isDir ? 'dir' : 'file') || updateSelected(['_new'])
72 | }
73 | onRenameItem={() => updateRenameMode(true)}
74 | onDeleteItem={onDeleteItem}
75 | onSelectAll={() => updateSelected(directoryData.map((item) => item.id))}
76 | onSelectNone={() => updateSelected([])}
77 | onCopyItems={() => updateClipboard({ mode: 1, items: [...selectedItems] })}
78 | onCutItems={() => updateClipboard({ mode: 0, items: [...selectedItems] })}
79 | onPasteItems={onPasteItems}
80 | />
81 |
82 |
83 |
chooseCategory(id)}
88 | />
89 |
90 |
91 |
105 |
106 |
107 |
108 | );
109 | };
110 |
111 | export default Explorer;
112 |
--------------------------------------------------------------------------------
/src/pages/Program/Program.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react';
2 | import Draggable from 'react-draggable';
3 |
4 | import TitleBar from '../../components/TitleBar/TitleBar';
5 | import MenuBar from '../../components/MenuBar/MenuBar';
6 | import { getProgramData, initDisk, updateProgramData } from '../../disk';
7 | import './Program.scss';
8 |
9 | const { innerWidth: windowWidth, innerHeight: windowHeight } = window;
10 | const leftOffset = (w) => (windowWidth - w) * 0.25;
11 | const topOffset = (h) => (windowHeight - h) * 0.5;
12 | const defaultHeight = '480px';
13 | const defaultWidth = '640px';
14 |
15 | const ProgramComponent = React.memo(
16 | ({ component, ...props }) => component(props),
17 | () => true
18 | );
19 |
20 | const Program = React.memo(
21 | (props) => {
22 | const { app } = props;
23 | const initWindowHeight = app.config?.initWindowHeight || defaultHeight;
24 | const initWindowWidth = app.config?.initWindowWidth || defaultWidth;
25 | const programRef = useRef(null);
26 | const [disk] = useState(initDisk(app.id, app.version || 1));
27 |
28 | // bring the program to foreground on componentDidMount
29 | useEffect(() => props.onClickWindow(app.pId), []);
30 |
31 | const [dimensions, updateDimensions] = useState({
32 | delta: {
33 | x: leftOffset(initWindowWidth),
34 | y: topOffset(initWindowHeight),
35 | reset: false,
36 | },
37 | defaultStyle: { height: initWindowHeight, width: initWindowWidth },
38 | style: { height: initWindowHeight, width: initWindowWidth },
39 | isMaximized: false,
40 | });
41 |
42 | const handleDrag = (e, ui) => {
43 | const { x, y } = dimensions.delta;
44 | if (!dimensions.isMaximized)
45 | updateDimensions({ ...dimensions, delta: { x: x + ui.deltaX, y: y + ui.deltaY } });
46 | };
47 |
48 | const maximize = () => {
49 | updateDimensions({
50 | ...dimensions,
51 | style: { top: '0', bottom: '48px', left: '0', right: '0' },
52 | delta: { ...dimensions.delta, reset: true },
53 | isMaximized: true,
54 | });
55 | };
56 |
57 | const restore = () => {
58 | updateDimensions({
59 | ...dimensions,
60 | style: { ...dimensions.defaultStyle },
61 | delta: { ...dimensions.delta, reset: false },
62 | isMaximized: false,
63 | });
64 | };
65 |
66 | return (
67 |
74 | props.onClickWindow(app.pId)}
83 | >
84 |
props.onMinimize(app.pId)}
90 | onMaximize={maximize}
91 | onRestore={restore}
92 | onTerminate={() => props.onTerminate(app.pId)}
93 | />
94 |
95 |
96 |
true}
100 | createMenubar={(items) => }
101 | updateProgramData={updateProgramData(disk)}
102 | getProgramData={getProgramData(disk)}
103 | onTerminate={() => props.onTerminate(app.id)}
104 | />
105 |
106 |
107 |
108 | );
109 | },
110 | (prevProps, nextProps) =>
111 | // in case minimized state is same, it should be in the background or foreground in prev and new state
112 | prevProps.isMinimized === nextProps.isMinimized &&
113 | ((prevProps.zIndex === 'auto' && nextProps.zIndex === 'auto') ||
114 | (prevProps.zIndex !== 'auto' && nextProps.zIndex !== 'auto'))
115 | );
116 |
117 | export default Program;
118 |
--------------------------------------------------------------------------------
/src/components/StartMenu/StartMenu.scss:
--------------------------------------------------------------------------------
1 | @use '../../scss/mixins';
2 | @import '../../scss/variables';
3 |
4 | $startMenuHeight: 512px;
5 | $startMenuBottomOffset: 48px;
6 | $startMenuAppsWidth: 320px;
7 | $startMenuPromotionsWidth: 660px;
8 | $startMenuPromotionsLargeWidth: $startMenuPromotionsWidth / 3;
9 | $startMenuPromotionsSmallWidth: $startMenuPromotionsWidth / 6;
10 |
11 |
12 | .StartMenu {
13 | position: absolute;
14 | left: 0;
15 | bottom: $startMenuBottomOffset;
16 | font-size: 16px;
17 | height: $startMenuHeight;
18 | padding-top: 16px;
19 | display: flex;
20 | align-items: flex-start;
21 | background: $taskBarBg;
22 | color: $taskBarFg;
23 | transition: bottom 0.5s ease, width 0.5s ease;
24 | z-index: 9999;
25 |
26 | &-account[active='false'] {
27 | width: 48px;
28 | }
29 |
30 | &-account {
31 |
32 | width: $startMenuAppsWidth;
33 | height: 100%;
34 | transition: width .5s ease;
35 | overflow: hidden;
36 | display: flex;
37 | flex-direction: column;
38 | justify-content: space-between;
39 | background: $taskBarBg;
40 | z-index: 99;
41 |
42 | &-nav &-list-item-label {
43 | @include mixins.fonttype('semibold');
44 | }
45 |
46 | &-list {
47 | &-item {
48 | height: 53.21px;
49 | padding: 16px;
50 | width: 100%;
51 | display: flex;
52 | align-items: center;
53 |
54 | &-label {
55 | margin-left: 32px;
56 | text-overflow: clip;
57 | white-space: nowrap;
58 | overflow: hidden;
59 | }
60 |
61 | &-icon {
62 | height: 100%;
63 | font-size: 1.25em;
64 | display: flex;
65 | align-items: center;
66 | }
67 | }
68 | }
69 | }
70 |
71 | &-apps {
72 | position: absolute;
73 | top: 0;
74 | left: 48px;
75 | width: $startMenuAppsWidth;
76 | padding: 16px 16px 0 16px;
77 | background: $taskBarBg;
78 | height: 100%;
79 |
80 | &-category {
81 | padding-top: 16px;
82 |
83 | &-header {
84 | @include mixins.fonttype('semibold');
85 | padding-bottom: 16px;
86 | padding-top: -4px;
87 | }
88 |
89 | &-list {
90 | &-item {
91 | display: flex;
92 | align-items: center;
93 | cursor: default;
94 | margin-right: 48px;
95 | padding: 2px 2px 2px 0;
96 | border: 2px solid transparent;
97 |
98 | &-icon {
99 | padding: 8px;
100 | background: #3e3c91;
101 | font-size: 20px;
102 | display: flex;
103 | align-items: center;
104 | justify-content: center;
105 | width: 40px;
106 | }
107 |
108 | &-label {
109 | margin-left: 8px;
110 | }
111 | }
112 |
113 | &-item:active {
114 | border: 2px solid white;
115 | }
116 | }
117 | }
118 | }
119 |
120 | &-promotions {
121 | position: absolute;
122 | top: 0;
123 | left: $startMenuAppsWidth;
124 | width: $startMenuPromotionsWidth + 64px;
125 | height: $startMenuHeight;
126 | padding-top: 16px;
127 | background: $taskBarBg;
128 | display: flex;
129 |
130 | > * {
131 | margin-top: 16px;
132 | flex: 1;
133 | padding-right: 16px;
134 | }
135 |
136 | &-header {
137 | @include mixins.fonttype('semibold');
138 | padding-bottom: 16px;
139 | }
140 |
141 | &-list {
142 | display: flex;
143 | flex-wrap: wrap;
144 |
145 | a {
146 | color: white;
147 | text-decoration: none;
148 | }
149 |
150 | &-item {
151 | height: $startMenuPromotionsSmallWidth;
152 | position: relative;
153 | display: flex;
154 | margin-right: 4px;
155 | margin-bottom: 4px;
156 |
157 | &-icon {
158 | margin: auto;
159 | font-size: 32px;
160 | }
161 |
162 | &-label {
163 | font-size: 12px;
164 | position: absolute;
165 | left: 0;
166 | padding: 0 4px;
167 | bottom: 8px;
168 | width: 100%;
169 | text-align: center;
170 | }
171 |
172 | }
173 |
174 | &-item[wsize='large'] {
175 | width: $startMenuPromotionsLargeWidth + 4px;
176 | }
177 |
178 | &-item[wsize='small'] {
179 | width: $startMenuPromotionsSmallWidth;
180 | }
181 | }
182 | }
183 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | mindows
2 | A minimal Operating System created with React.
3 |
4 |
5 |
6 | ## About
7 | This project provides a minimal Windows 10 experience in browser. It runs completely on client's side. The user is presented with screen to create a new user account on the first visit. User can create multiple accounts, and log into any one of them. Desktop, TaskBar and StartMenu are shown after logging in. Basic apps like Notepad (text editor) and File Explorer are pre-built into the app. Windows 10 design is tried to be replicated for both apps.
8 |
9 | New documents and folders can be created in File Explorer. Explorer ribbon similar to that in Windows 10 is also provided for Copy/Cut/Paste/Select/Open operations. Furthermore, project is designed in a way to add new apps without affecting the project structure. It's as simple as writing a new app, add it into ```softwares``` folder and edit the ```config/apps.js``` file. Functions like reading/writing filesystem (yeppp, not the real OS filesystem but the browser storage), etc. are automatically passed down to the app.
10 |
11 | Filesystem is built using [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) and [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) API of browsers. So, incase the app data is deleted, complete accounts' data are deleted... like after a new OS installation.
12 |
13 | It's not complete but it's honest work.
14 |
15 | ## Features
16 | - Windows 10 user interface
17 | - Persistent Filesystem
18 | - Multiple user accounts
19 | - Multiple instances of apps
20 | - Maximizable, minimizable, draggable and focusable apps instances
21 | - Custom installable apps
22 | - Different apps for different types of documents
23 | - Separation of apps based on permissions
24 |
25 | ## Contribution
26 | This is my first project of this level, so it might have bugs. New apps (what about a music player?) can be written and added to the project. For now apps including Calender, Calculator and Settings are not pre-installed into it.
27 |
28 | ## Getting Started with Create React App
29 |
30 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). Follow the steps at [ReactJS](https://github.com/facebook/react) for more information about running this project locally.
31 |
32 | ## Credits
33 | Wallpapers used in the project are taken from different websites.
34 | - [Windows 10 Official Wallpaper](http://wallpaperswide.com/windows_10_hero_4k-wallpapers.html)
35 | - [Wharariki Beach Cave, Archway Islands, South Island of New Zealand](https://windows10spotlight.com/images/9fa80fd805562a6bc817f01f48b8b93e)
36 |
37 | The windows 10 loading icon has also been inserted from [codepen created by Fernando de Almeida Faria](https://codepen.io/feebaa/pen/PPrLQP).
38 |
39 | ## Used Libraries
40 | - [ReactJS](https://reactjs.org/)
41 | - [React Redux](https://react-redux.js.org/)
42 | - [React Router](https://reactrouter.com/)
43 | - [React Icons](https://react-icons.github.io/react-icons/)
44 | - [React Draggable](https://github.com/react-grid-layout/react-draggable)
45 | - [Sass](https://sass-lang.com/)
46 |
47 | ## Demo Video
48 | https://user-images.githubusercontent.com/20419286/130333497-d30219b2-feb7-44f2-af92-61731db6d98f.mp4
49 |
50 | ## Screenshots
51 | **Boot Screen**
52 |
53 |
54 |
55 |
56 | **New User Account Screen**
57 |
58 |
59 |
60 |
61 | **Switch User Screen**
62 |
63 |
64 |
65 |
66 | **Desktop**
67 |
68 |
69 |
70 |
71 | **Desktop with Start Menu**
72 |
73 |
74 |
75 |
76 | **File Explorer**
77 |
78 |
79 |
80 |
81 | **Notepad**
82 |
83 |
84 |
85 |
86 | **Logout Screen**
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/src/pages/Desktop/Desktop.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 |
4 | import TaskBar from '../../components/TaskBar/TaskBar';
5 | import StartMenu from '../../components/StartMenu/StartMenu';
6 | import Program from '../Program/Program';
7 | import InstalledApps from '../../config/apps';
8 | import {
9 | selectAccountSettings,
10 | selectDefaultApps,
11 | selectTaskBarApps,
12 | } from '../../redux/account/account.selectors';
13 | import { loadAccount, saveAccount } from '../../redux/account/account.actions';
14 | import { startNewProgram, terminateProgram } from '../../redux/memory/memory.action';
15 | import { selectAppsInstances, selectProgramsData } from '../../redux/memory/memory.selectors';
16 | import './Desktop.scss';
17 | import Loading from '../../components/Loading/Loading';
18 | import { logOut } from '../../redux/auth/auth.actions';
19 |
20 | const Desktop = ({ activeUser }) => {
21 | const accountSettings = useSelector(selectAccountSettings);
22 | const dispatch = useDispatch();
23 | const [startMenu, toggleStartMenu] = useState(false);
24 | const [opacity, updateOpacity] = useState(1);
25 | const [logoutMode, startLogoutMode] = useState(false);
26 | const style = {
27 | opacity,
28 | backgroundImage: `url(${process.env.PUBLIC_URL}"/images/${accountSettings.background}")`,
29 | };
30 |
31 | useEffect(() => dispatch(loadAccount(activeUser)), []);
32 | useEffect(() => {
33 | if (!opacity) {
34 | setTimeout(() => {
35 | startLogoutMode(true);
36 | updateOpacity(1);
37 | setTimeout(() => {
38 | dispatch(saveAccount(activeUser));
39 | dispatch(logOut());
40 | }, 4000);
41 | }, 400);
42 | }
43 | }, [opacity]);
44 |
45 | // programsData: { pId_1: programData, pId_2: programData }
46 | // appsInstances: { calculator: [pId_1, .., pId_k], calendar: [pId_1,..., pId_m] }
47 | // runningPrograms: [pId_1, pId_2, ...]
48 | const programsData = useSelector(selectProgramsData);
49 | const appsInstances = useSelector(selectAppsInstances);
50 | const runningPrograms = Object.keys(programsData);
51 | const [windows, updateWindows] = useState({ maxZIndex: 100, pId: null });
52 | const [minimized, updateMinimized] = useState({});
53 |
54 | // taskbarApps: [calculator, calendar, ...]
55 | const taskbarApps = useSelector(selectTaskBarApps);
56 | const taskbarAndOpenedApps = taskbarApps.concat(Object.keys(appsInstances));
57 | const taskbarAppsData = [...new Set(taskbarAndOpenedApps)].map((appId) => InstalledApps[appId]);
58 |
59 | // defaultApps
60 | const defaultApps = useSelector(selectDefaultApps);
61 |
62 | // bring activated window/program to the foreground
63 | const onClickProgramWindow = (pId) =>
64 | updateWindows(({ maxZIndex }) => ({ maxZIndex: maxZIndex + 1, pId }));
65 |
66 | // add a program to minimized state
67 | const onToggleMinimize = (pId, to) => {
68 | const newMinimized = {};
69 | newMinimized[pId] = to;
70 | updateMinimized(Object.assign(minimized, newMinimized));
71 | };
72 |
73 | // when program is selected from taskbar
74 | const onSelectFromTaskBar = (pId) => {
75 | onClickProgramWindow(pId);
76 | onToggleMinimize(pId, false);
77 | };
78 |
79 | // start a new program
80 | const onStartNewProgram = (app) => {
81 | toggleStartMenu(false);
82 | dispatch(startNewProgram(app));
83 | };
84 |
85 | // open a document from file explorer
86 | const onOpenDocument = (docId, name, ext = '*') => {
87 | const app = InstalledApps[defaultApps[ext] || defaultApps['*']];
88 | dispatch(startNewProgram(app, { docId, title: name }));
89 | };
90 |
91 | return (
92 |
93 | {logoutMode ? (
94 |
95 | ) : (
96 | <>
97 | {runningPrograms.map((pId) => (
98 |
onToggleMinimize(_pId, true)}
104 | onTerminate={() => dispatch(terminateProgram(pId))}
105 | onClickWindow={onClickProgramWindow}
106 | onOpenDocument={onOpenDocument}
107 | />
108 | ))}
109 |
110 | updateOpacity(0)}
114 | onProgramClick={(app) => onStartNewProgram(app)}
115 | />
116 |
117 | onSelectFromTaskBar(pId)}
122 | onIconClick={(app) => onStartNewProgram(app)}
123 | onMindowsClick={() => toggleStartMenu(!startMenu)}
124 | onCloseInstance={(pId) => dispatch(terminateProgram(pId))}
125 | />
126 | >
127 | )}
128 |
129 | );
130 | };
131 |
132 | export default Desktop;
133 |
--------------------------------------------------------------------------------
/src/softwares/Explorer/components/Ribbon/Ribbon.jsx:
--------------------------------------------------------------------------------
1 | import { GrCopy, GrDocument } from 'react-icons/gr';
2 | import { MdContentPaste } from 'react-icons/md';
3 | import { IoIosCut } from 'react-icons/io';
4 | import { ImCross } from 'react-icons/im';
5 | import { BiRename } from 'react-icons/bi';
6 | import { BsGrid, BsGridFill } from 'react-icons/bs';
7 | import { GoPencil } from 'react-icons/go';
8 |
9 | import './Ribbon.scss';
10 |
11 | const CategoryClipboard = ({
12 | disabledPaste,
13 | onCopyItems,
14 | onCutItems,
15 | onPasteItems,
16 | disabledCopyOrCut,
17 | }) => (
18 |
19 |
20 |
25 |
26 |
27 |
28 |
Copy
29 |
30 |
35 |
36 |
37 |
38 |
Paste
39 |
40 |
45 |
46 |
47 |
48 |
Cut
49 |
50 |
51 |
Clipboard
52 |
53 | );
54 |
55 | const CategoryOrganize = ({ onDeleteItem, onRenameItem, disabled }) => (
56 |
57 |
58 |
59 |
60 |
61 |
62 |
Delete
63 |
64 |
65 |
66 |
67 |
68 |
Rename
69 |
70 |
71 |
Organize
72 |
73 | );
74 |
75 | const CategoryNew = ({ onCreate }) => (
76 |
77 |
78 |
onCreate(true)}>
79 |
80 |

81 |
82 |
New Folder
83 |
84 |
onCreate(false)}>
85 |
86 |
87 |
88 |
New Item
89 |
90 |
91 |
New
92 |
93 | );
94 |
95 | const CategoryOpen = ({ disabled }) => (
96 |
97 |
98 |
99 |
100 |
101 |
102 |
Open
103 |
104 |
105 |
Open
106 |
107 | );
108 |
109 | const CategorySelect = ({ onSelectAll, onSelectNone }) => (
110 |
111 |
112 |
113 |
114 |
115 |
116 |
Select all
117 |
118 |
119 |
120 |
121 |
122 |
Select none
123 |
124 |
125 |
Select
126 |
127 | );
128 |
129 | const Ribbon = ({
130 | disableAll,
131 | selectedItems,
132 | isClipboardEmpty,
133 | onCreateItem,
134 | onRenameItem,
135 | onDeleteItem,
136 | onSelectAll,
137 | onSelectNone,
138 | onCopyItems,
139 | onCutItems,
140 | onPasteItems,
141 | }) => {
142 | const isFileSelected =
143 | selectedItems.length === 0 || (selectedItems.length === 1 && selectedItems[0] === '_new');
144 |
145 | return (
146 |
147 |
154 |
155 |
160 |
161 | onCreateItem(isDir)} />
162 |
163 |
164 |
165 |
166 |
167 | );
168 | };
169 |
170 | export default Ribbon;
171 |
--------------------------------------------------------------------------------
/src/components/StartMenu/StartMenu.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { VscMenu, VscFolder, VscSettingsGear } from 'react-icons/vsc';
3 | import { AiOutlineUser } from 'react-icons/ai';
4 | import { RiShutDownLine } from 'react-icons/ri';
5 |
6 | import Promotions from './Promotions';
7 | import InstalledApps from '../../config/apps';
8 | import { getName } from '../../redux/auth/auth.user';
9 | import './StartMenu.scss';
10 |
11 | const StartMenuDrawer = React.memo(
12 | ({ user, onLogout, onLaunchApp }) => {
13 | const [drawer, toggleDrawer] = useState(false);
14 |
15 | return (
16 |
17 |
18 |
19 |
toggleDrawer(!drawer)}>
20 |
21 |
22 |
START
23 |
24 |
25 |
26 |
27 |
30 |
{getName(user)}
31 |
32 |
onLaunchApp(InstalledApps.fsexplorer)}
35 | >
36 |
37 |
38 |
39 |
File Explorer
40 |
41 |
onLaunchApp(InstalledApps.settings)}
44 | >
45 |
46 |
47 |
48 |
Settings
49 |
50 |
51 |
52 |
53 |
54 |
Log Off
55 |
56 |
57 |
58 | );
59 | },
60 | () => true
61 | );
62 |
63 | const StartMenuApps = React.memo(
64 | ({ onClick }) => {
65 | const Apps = ({ apps, category }) =>
66 | apps.map((app, i) => (
67 | onClick(app)}
70 | className="StartMenu-apps-category-list-item"
71 | >
72 |
{app.icon()}
73 |
{app.name}
74 |
75 | ));
76 |
77 | const Category = ({ category }) => (
78 |
79 |
{category.category}
80 |
83 |
84 | );
85 |
86 | const smApps = [];
87 | const apps = Object.keys(InstalledApps).map((id) => InstalledApps[id]);
88 | [...new Set(apps.map((app) => app.name[0].toLowerCase()))].sort().forEach((c) => {
89 | smApps.push({
90 | category: c.toUpperCase(),
91 | apps: apps.filter((app) => app.name[0].toLowerCase() === c),
92 | });
93 | });
94 |
95 | return (
96 |
97 | {smApps.map((cat) => (
98 |
99 | ))}
100 |
101 | );
102 | },
103 | () => true
104 | );
105 |
106 | const StartMenuPromotions = React.memo(
107 | () => {
108 | let promotions = [...Promotions];
109 | for (let i = 0; i < promotions.length - 1; i += 1) {
110 | const j = Math.floor(Math.random * promotions.length) % promotions.length;
111 | [promotions[i], promotions[j]] = [promotions[j], promotions[i]];
112 | }
113 | promotions = promotions.filter((pr) => pr);
114 | const colors = ['#3e3c91', 'green', 'orange', 'red'];
115 |
116 | const Blocks = ({ list }) =>
117 | list.map((item, i) => (
118 |
119 |
124 |
{item.icon()}
125 |
{item.text}
126 |
127 |
128 | ));
129 |
130 | return (
131 |
132 |
133 |
Life at a glance
134 |
135 |
136 |
137 |
138 |
139 |
Explore
140 |
141 |
142 |
143 |
144 |
145 | );
146 | },
147 | (prevProps, nextProps) => nextProps.hide
148 | );
149 |
150 | const StartMenu = React.memo(
151 | ({ user, hide, onLogout, onProgramClick }) => (
152 |
153 |
154 |
155 |
156 |
157 | ),
158 | (prevProps, nextProps) => prevProps.hide === nextProps.hide
159 | );
160 |
161 | export default StartMenu;
162 |
--------------------------------------------------------------------------------