init the awkward code

This commit is contained in:
Bao Nguyen
2023-02-13 19:32:10 +07:00
commit 27170afcac
5426 changed files with 1244579 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
.npmrc
!.env.example
.vscode/
build.zip

4
README.md Normal file
View File

@@ -0,0 +1,4 @@
# win32.run
### Yet another fake Windows XP in the browser, but with a File System and comes with it, Programs.
Microsoft and Windows XP trademarks & logos definitely belong to Microsoft Corporation. All the programs' names and logos (Foxit, Word, WinRar, Internet Explorer, etc.) are of their rightful copyright holders.
**win32.run** is purely for the purpose of nostalgia. I have no intent and no right to monetize win32.run, but you may occasionally see ads when playing third-party games.

58
gen/assets.js Normal file
View File

@@ -0,0 +1,58 @@
import path from 'path';
import dir from 'node-dir';
import fs from 'fs';
let excluded_source_files = ['src/routes/xp/starting.svelte'];
let source_files = [
...dir.files('./src/', {sync: true}),
'static/json/hard_drive.json',
'svelte.config.js',
'tailwind.config.cjs',
'vite.config.js'
]
.filter(el => ['.js', '.json', '.svelte', '.css', '.cjs', '.html'].includes(path.extname(el)))
.filter(el => !excluded_source_files.includes(el));
(async () => {
let remote_files = dir.files('./static/files/', {sync: true})
.filter(file => ['.png', '.jpg', '.mp3'].includes(path.extname(file)))
.filter(file => included(file))
.map(file => file.replace(/^static/i, ''));
let images = dir.files('./static/images/', {sync: true})
.filter(file => ['.png', '.jpg', '.svg', '.gif'].includes(path.extname(file)))
.filter(file => included(file))
.map(file => file.replace(/^static/i, ''));
let fonts = dir.files('./static/fonts/', {sync: true})
.filter(file => ['.ttf'].includes(path.extname(file)))
.filter(file => included(file))
.map(file => file.replace(/^static/i, ''));
let audios = dir.files('./static/audio/', {sync: true})
.filter(file => ['.mp3', '.wav'].includes(path.extname(file)))
.filter(file => included(file))
.map(file => file.replace(/^static/i, ''));
let empties = dir.files('./static/empty/', {sync: true})
.filter(file => included(file))
.map(file => file.replace(/^static/i, ''));
let assets = {remote_files, images, audios, fonts, empties};
for(let key of Object.keys(assets)){
console.log('let ' + key + ' = ' + JSON.stringify(assets[key]) + ';\n');
}
})()
function included(asset){
let basename = path.basename(asset);
for(let file of source_files){
let content = fs.readFileSync(file, 'utf-8');
if(content.includes(basename)) return true;
}
return false;
}

16
gen/imports.js Normal file
View File

@@ -0,0 +1,16 @@
import path from 'path';
import dir from 'node-dir';
(async () => {
let files = dir.files('./src/routes/', {sync: true}).filter(file => path.extname(file) == '.svelte');
let statements = '';
for(let file of files){
let import_path = file.split('src/routes/').join('./')
statements = statements + `
else if(url == '${import_path}'){
page = (await import('${import_path}')).default;
}`
}
console.log(statements);
})()

7544
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

49
package.json Normal file
View File

@@ -0,0 +1,49 @@
{
"name": "web-svelte",
"version": "0.0.1",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"package": "svelte-kit package",
"preview": "vite preview"
},
"devDependencies": {
"@jhubbardsf/svelte-sortablejs": "^1.1.0",
"@sveltejs/adapter-auto": "next",
"@sveltejs/adapter-node": "^1.0.0-next.94",
"@sveltejs/kit": "next",
"autoprefixer": "^10.4.7",
"postcss": "^8.4.14",
"postcss-load-config": "^4.0.1",
"svelte": "^3.44.0",
"svelte-preprocess": "^4.10.7",
"tailwindcss": "^3.1.5",
"vite": "^3.0.4"
},
"type": "module",
"dependencies": {
"@faker-js/faker": "^7.5.0",
"@tailwindcss/line-clamp": "^0.4.2",
"@types/lodash": "^4.14.186",
"@vitejs/plugin-basic-ssl": "^1.0.1",
"axios": "^0.27.2",
"bcrypt": "^5.0.1",
"buffer": "^6.0.3",
"build-url": "^6.0.1",
"docx": "^7.7.0",
"dragselect": "^2.5.5",
"fast-fuzzy": "^1.11.2",
"file-saver": "^2.0.5",
"idb-keyval": "^6.2.0",
"is-valid-http-url": "^1.0.3",
"jsdom": "^20.0.3",
"jszip": "^3.10.1",
"link-preview-js": "^3.0.4",
"lodash": "^4.17.21",
"node-dir": "^0.1.17",
"short-uuid": "^4.2.2",
"srt-webvtt": "^2.0.0",
"svelte-range-slider-pips": "^2.0.3",
"tailwindcss-scoped-groups": "^2.0.0"
}
}

13
postcss.config.cjs Normal file
View File

@@ -0,0 +1,13 @@
const tailwindcss = require("tailwindcss");
const autoprefixer = require("autoprefixer");
const config = {
plugins: [
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
tailwindcss(),
//But others, like autoprefixer, need to run after,
autoprefixer,
],
};
module.exports = config;

225
src/app.css Normal file

File diff suppressed because one or more lines are too long

225
src/app.html Normal file
View File

@@ -0,0 +1,225 @@
<!DOCTYPE html>
<html lang="en" data-theme="" style="background:black">
<head>
<meta charset="utf-8" />
<title>Microsoft Windows XP Professional</title>
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link rel="stylesheet" id="theme" href=""/>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="description" content="Yet another Windows XP in the browser, but with a File System and comes with it, Programs."/>
%sveltekit.head%
<style>
html, body {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
section {
width: 100vw;
height: 100vh;
}
p {
margin: 0;
line-height: 1.5em;
}
.blink {
animation: blink-animation 1s steps(5, start) infinite;
-webkit-animation: blink-animation 1s steps(5, start) infinite;
}
@keyframes blink-animation {
to {
visibility: hidden;
}
}
@-webkit-keyframes blink-animation {
to {
visibility: hidden;
}
}
.actions {
position: fixed;
bottom: 0;
width: 100%;
}
.float-right {
position: fixed;
bottom: 0;
right: 0;
padding-right: 2em;
}
#bios {
display: block;
background: #000;
color: #ccc;
font-family: monospace;
}
#loader {
display: none;
background: #000;
color: #ccc;
}
.loader {
position: relative;
padding-top: 100px;
width: 40px;
margin: auto;
}
.loader .circle {
position: absolute;
width: 38px;
height: 38px;
opacity: 0;
transform: rotate(225deg);
animation-iteration-count: infinite;
animation-name: orbit;
animation-duration: 5.5s;
}
.loader .circle:after {
content: "";
position: absolute;
width: 5px;
height: 5px;
border-radius: 5px;
background: #fff;
/* Pick a color */
}
.loader .circle:nth-child(2) {
animation-delay: 240ms;
}
.loader .circle:nth-child(3) {
animation-delay: 480ms;
}
.loader .circle:nth-child(4) {
animation-delay: 720ms;
}
.loader .circle:nth-child(5) {
animation-delay: 960ms;
}
@keyframes orbit {
0% {
transform: rotate(225deg);
opacity: 1;
animation-timing-function: ease-out;
}
7% {
transform: rotate(345deg);
animation-timing-function: linear;
}
30% {
transform: rotate(455deg);
animation-timing-function: ease-in-out;
}
39% {
transform: rotate(690deg);
animation-timing-function: linear;
}
70% {
transform: rotate(815deg);
opacity: 1;
animation-timing-function: ease-out;
}
75% {
transform: rotate(945deg);
animation-timing-function: ease-out;
}
76% {
transform: rotate(945deg);
opacity: 0;
}
100% {
transform: rotate(945deg);
opacity: 0;
}
}
</style>
</head>
<body>
<div id="iframe-preload" style="position: absolute;inset: 0;display: none;"></div>
<div style="position:absolute;inset:0;">%sveltekit.body%</div>
<div id="pos_loader" style="position:absolute;inset:0;padding:10px;background-color:black;font-size: 18px;">
<!-- Bootup by Kyle Stephens -->
<!-- https://codepen.io/kylestephens/pen/zYOgLrr -->
<section id="bios">
<p>PhoenixBIOS 1.4 Release 6.0</p>
<p>Copyright 1985-2001 Phoenix Technologies Ltd.</p>
<p>All Rights Reserved</p>
<p>Copyright 2001-2003 VMware. Inc.</p>
<p>VMware BIOS build 314</p>
<br />
<p>ATAPI CD-ROM: VMware Virtual IDECDROM Drive</p>
<p>Initializing <span class="blink">...</span></p>
</section>
<section id="loader">
<div class='loader'>
<div class='circle'></div>
<div class='circle'></div>
<div class='circle'></div>
<div class='circle'></div>
<div class='circle'></div>
</div>
</section>
</div>
<script>
window.addEventListener('contextmenu', (event) => {
event.preventDefault();
})
</script>
<script src="//unpkg.com/loadjs@latest/dist/loadjs.min.js"></script>
<script>
function load_assets(assets, completion){
Promise
.all(assets.map(asset=>fetch(asset)))
.then(responses =>
Promise.all(responses.map(res => res.blob()))
).then(completion);
}
load_assets([
'/images/xp_loading_logo.jpg',
'/images/xp_loading_mslogo.jpg',
'/images/xp_logo_horizontal.png',
'/images/xp_logo.png',
'/fonts/levi.ttf',
'/fonts/ms_sans_serif_bold.ttf',
'/fonts/ms_sans_serif.ttf'], () => {
document.querySelector('#pos_loader').style.display = 'none';
})
</script>
<script>
document.addEventListener('dragover', function(e){
e.preventDefault();
console.log('dragover');
})
document.addEventListener('drop', function(e){
e.preventDefault();
console.log('drop');
})
</script>
</body>
</html>

6
src/hooks.js Normal file
View File

@@ -0,0 +1,6 @@
export const handle = async ({ event, resolve }) => {
const response = await resolve(event, {
ssr: false
});
return response;
};

View File

@@ -0,0 +1,19 @@
<script>
export let on_click = (event) => {
}
export let title = '';
export let style = '';
export let disabled = false;
</script>
<button on:click={(event) => on_click(event)} disabled={disabled} style="
{style};
background: silver;
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;"
class="px-2 py-1 text-center min-w-[50px] min-h-[20px] font-MSSS text-sm text-black
focus:outline-dotted outline-black outline-offset-[-4px] disabled:text-gray-500">
{title}
</button>

View File

@@ -0,0 +1,39 @@
<script>
export let options = {};
let {close_btn=true, maximize_btn=true, minimize_btn=true,
close_btn_disabled=false, maximize_btn_disabled=false, minimize_btn_disabled=false,
title=''} = options;
export let on_click_maximize = () => {}
export let on_click_minimize = () => {}
export let on_click_close = () => {}
</script>
<div class="titlebar flex items-center justify-between bg-[linear-gradient(90deg,navy,#1084d0)] p-0.5 font-MSSS">
<p class="text-white font-semibold mr-4 text-sm ml-1 mb-1">{title}</p>
<div class="flex">
{#if minimize_btn}
<button disabled={minimize_btn_disabled} on:click={on_click_minimize}
class="group w-4 h-4 ml-1 bg-[#c0c0c0]" style="box-shadow:inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf">
<svg class="fill-black group-disabled:fill-gray-500 w-3 h-3 m-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M32 416c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H32z"/></svg>
</button>
{/if}
{#if maximize_btn}
<button disabled={maximize_btn_disabled} on:click={on_click_maximize}
class="group w-4 h-4 ml-1 bg-[#c0c0c0]" style="box-shadow:inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf">
<svg class="fill-black group-disabled:fill-gray-500 w-3 h-3 m-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M7.724 65.49C13.36 55.11 21.79 46.47 32 40.56C39.63 36.15 48.25 33.26 57.46 32.33C59.61 32.11 61.79 32 64 32H448C483.3 32 512 60.65 512 96V416C512 451.3 483.3 480 448 480H64C28.65 480 0 451.3 0 416V96C0 93.79 .112 91.61 .3306 89.46C1.204 80.85 3.784 72.75 7.724 65.49V65.49zM48 416C48 424.8 55.16 432 64 432H448C456.8 432 464 424.8 464 416V224H48V416z"/></svg>
</button>
{/if}
{#if close_btn}
<button disabled={close_btn_disabled} on:click={on_click_close}
class="group w-4 h-4 ml-1 bg-[#c0c0c0]" style="box-shadow:inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf">
<svg class="fill-black group-disabled:fill-gray-500 w-3 h-3 m-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M310.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L160 210.7 54.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L114.7 256 9.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 301.3 265.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L205.3 256 310.6 150.6z"/></svg>
</button>
{/if}
</div>
</div>
<svelte:options accessors={true}></svelte:options>

View File

@@ -0,0 +1,125 @@
<script>
import {onMount} from 'svelte';
import TitleBar from './TitleBar.svelte';
import * as utils from '../../utils';
import _ from 'lodash';
export let self;
export let options = {something: 'some value'};
let node_ref;
let saved_position;
let maximized;
let minimized;
onMount(() => {
if(options.top == null){
options.top = (node_ref.parentNode.offsetHeight - node_ref.offsetHeight)/2;
}
if(options.left == null){
options.left = (node_ref.parentNode.offsetWidth - node_ref.offsetWidth)/2;
}
set_position({top: options.top, left: options.left, width: node_ref.width, height: node_ref.height});
// enable_resize();
enable_drag();
})
let on_click_close = () => {
self.$destroy();
}
export let on_click_maximize = () => {
if(maximized){
set_position(saved_position);
maximized = false;
} else {
let rect = utils.relative_rect(node_ref.parentNode.getBoundingClientRect(), node_ref.getBoundingClientRect());
console.log(rect);
saved_position = {top: rect.top, left: rect.left, width: node_ref.offsetWidth, height: node_ref.offsetHeight};
set_position({top: 0, left: 0, width: node_ref.parentNode.offsetWidth, height: node_ref.parentNode.offsetHeight});
maximized = true;
}
}
function set_position({top, left, width, height, absolute_values}){
if(absolute_values){
let parent_top = node_ref.parentNode.getBoundingClientRect().top;
let parent_left = node_ref.parentNode.getBoundingClientRect().left;
top = top - parent_top;
left = left - parent_left;
}
node_ref.style.top = `${top}px`;
node_ref.style.left = `${left}px`;
node_ref.style.width = `${width}px`;
node_ref.style.height = `${height}px`;
}
function enable_resize(){
interact(node_ref)
.resizable({
edges: { top: true, left: true, bottom: true, right: true },
listeners: {
move: function (event) {
let { x, y } = event.target.dataset;
x = (parseFloat(x) || 0) + event.deltaRect.left;
y = (parseFloat(y) || 0) + event.deltaRect.top;
let rect = node_ref.getBoundingClientRect();
set_position({top: y + rect.top, left: x + rect.left, width: event.rect.width, height: event.rect.height, absolute_values: true});
}
},
modifiers: [
// keep the edges inside the parent
interact.modifiers.restrictEdges({
outer: 'parent'
}),
// minimum size
interact.modifiers.restrictSize({
min: { width: 100, height: 50 }
})
],
})
}
function enable_drag(){
jQuery(node_ref).draggable({
containment: 'parent',
handle: '.titlebar'
})
jQuery(node_ref).resizable({
minWidth: options.min_width,
minHeight: options.min_height,
containment: 'parent',
handles: 'all',
classes: {
'ui-resizable-se': ''
}
})
}
</script>
<div bind:this={node_ref} style="
position: absolute;
background: silver;
box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #dfdfdf, inset -2px -2px grey, inset 2px 2px #fff;
padding: 3px" class="absolute flex flex-col"
style:min-width="{options.min_width}px" style:min-height="{options.min_height}px">
<div class="shrink-0">
<TitleBar options={options}
on_click_close={on_click_close} on_click_maximize={on_click_maximize}>
</TitleBar>
</div>
<div class="grow shrink-0 relative">
<slot name="content"></slot>
</div>
</div>

View File

@@ -0,0 +1,28 @@
<script>
export let style = '';
export let items = [];
export let group_class = 'group';
</script>
<div class="absolute left-[90%] bottom-0 w-[250px] shadow-xl border-t border-l-4 border-blue-500 hidden group-sub1-hover:block bg-slate-50">
{#each items as item}
{#if item == null}
<div class="my-0.5 mx-auto w-5/6 h-[1px] bg-slate-200 shrink-0"></div>
{:else}
<div class="flex flex-row items-center grow p-1 group-sub2 hover:bg-blue-500 relative">
<div class="w-5 h-5 bg-contain mr-1 shrink-0"
style:background-image="url({item.icon})">
</div>
<div class="text-[11px] text-slate-800 grow group-sub12hover:text-white">
{item.name}
</div>
<div class="w-[10px] shrink-0">
{#if item.items != null}
<svg class="fill-slate-900 group-sub2-hover:fill-slate-50 w-[10px] h-[10px]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M246.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-128-128c-9.2-9.2-22.9-11.9-34.9-6.9s-19.8 16.6-19.8 29.6l0 256c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l128-128z"/></svg>
{/if}
</div>
</div>
{/if}
{/each}
</div>

View File

@@ -0,0 +1,42 @@
<script>
export let l_message = '';
export let r_message = '';
export let show_separator = false;
export async function display(messages){
for(let {l, r, d} of messages){
if(!l) l = '';
if(!r) r = '';
if(!d) d = 1000;
l_message = l;
r_message = r;
await sleep(d);
}
}
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
</script>
<div class="absolute bottom-0 left-0 right-0 h-8 bg-slate-200 text-slate-900 flex flex-row overflow-hidden font-MSSS">
<div class="grow h-full pb-1 font-bold px-4">
{l_message}
</div>
{#if show_separator}
<div class="w-1 h-full bg-slate-900"></div>
{/if}
<div class="basis-1/4 shrink-0 pb-1 font-bold px-4">
{r_message}
</div>
</div>
<svelte:options accessors={true}></svelte:options>

View File

@@ -0,0 +1,13 @@
<script>
export let show = false;
</script>
<div class="absolute top-0 left-0 right-0 bottom-0 w-screen h-screen bg-black overflow-hidden {show ? '' : 'hidden'} font-MSSS">
<div class="mt-12 ml-8 text-lg">
<div class="w-6 h-1 animate-cursor bg-slate-50">
</div>
</div>
</div>
<svelte:options accessors={true}></svelte:options>

View File

@@ -0,0 +1,54 @@
<script>
import {onMount} from 'svelte';
export let on_click = (event) => {}
export let title = '';
export let style = '';
export let disabled = false;
export let focus = false;
let node_ref;
onMount(() => {
if(focus){
node_ref.focus();
}
})
</script>
<button bind:this={node_ref} on:click={(event) => on_click(event)}
disabled={disabled} style="{style};" class="button disabled:opacity-30">
{title}
</button>
<style>
.button {
font-family: 'MSSS', Arial, Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 11px;
box-sizing: border-box;
border: 1px solid #003c74;
background: linear-gradient(180deg,#fff,#ecebe5 86%,#d8d0c4);
box-shadow: none;
border-radius: 3px;
min-width: 75px;
min-height: 23px;
text-align: center;
}
.button:focus {
outline: 1px dotted #000;
outline-offset: -4px;
box-shadow: inset -1px 1px #cee7ff, inset 1px 2px #98b8ea, inset -2px 2px #bcd4f6, inset 1px -1px #89ade4, inset 2px -2px #89ade4;
}
button:not(:disabled):hover {
box-shadow: inset -1px 1px #fff0cf, inset 1px 2px #fdd889, inset -2px 2px #fbc761, inset 2px -2px #e5a01a;
}
</style>
<svelte:options accessors={true}></svelte:options>

View File

@@ -0,0 +1,28 @@
<script>
export let style = '';
export let checkmark = true;
export let size = 12;
export let checked = false;
export let label = "";
function on_click(){
checked = !checked;
}
</script>
<div class="flex flex-row items-start" on:click={on_click} style="{style}">
<div class="border border-slate-500 bg-slate-50 inline-block box-content"
style:width="{size}px" style:height="{size}px">
{#if checked}
{#if checkmark}
<img alt="" src="/images/xp/checkmark.png" style:width="{size-2}px" style:height="{size-2}px" style:margin="1px">
{:else}
<div class="bg-gradient-to-r from-green-600 to-green-500 block"
style:width="{size-4}px" style:height="{size-4}px" style:margin="2px">
</div>
{/if}
{/if}
</div>
<span class="text-[11px] ml-1">{label}</span>
</div>

View File

@@ -0,0 +1,128 @@
<script>
let menu = [];
let required_width = 0;
let required_height = 0;
import { click_outside } from '../../utils';
import { contextMenu } from '../../store';
import { onDestroy } from 'svelte';
import { filter } from 'lodash';
let top = 0;
let left = 0;
let visible = false;
let screenWidth = 700;
let screenHeight = 700;
let unsubscriber = contextMenu.subscribe(async obj => {
if(obj == null){
visible = false;
return;
}
let {x, y, originator, type } = obj;
if(type == null) return;
let menu_obj;
if(type == 'ProgramTile'){
menu_obj = (await import('./context_menu/CMProgramTile')).make({type, originator});
} else if(type == 'Desktop'){
menu_obj = (await import('./context_menu/CMDesktop')).make({type, originator});
} else if(type == 'FSItem'){
menu_obj = (await import('./context_menu/CMFSItem')).make({type, originator});
} else if(type == 'FSVoid'){
menu_obj = (await import('./context_menu/CMFSVoid')).make({type, originator});
} else if(type == 'RecycleBin'){
menu_obj = (await import('./context_menu/RecycleBin')).make({type, originator});
}
menu = menu_obj.menu;
required_width = menu_obj.required_width;
required_height = menu_obj.required_height;
screenWidth = document.body.offsetWidth;
screenHeight = document.body.offsetHeight;
left = Math.min(x, screenWidth - required_width);
top = Math.min(y, screenHeight - required_height);
visible = true;
})
onDestroy(() => {
unsubscriber();
})
export let hide = () => {
visible = false;
contextMenu.set(null);
}
</script>
<div use:click_outside on:click_outside={() => hide()}
class="context-menu z-20 pt-0.5 absolute border-2 border-slate-200 bg-slate-50 text-slate-900 w-[180px] text-[11px] {visible ? '' : 'hidden' }"
style:top="{top}px" style:left="{left}px">
{#each menu.filter(el => el.length > 0) as group, group_index}
<div class="w-full border-slate-200 {group_index == menu.length - 1 ? '' : 'border-b'}">
{#each group as item}
<div class="py-1 w-full flex flex-row items-center {item.disabled? '' : 'hover:bg-blue-600'} relative group
{item.disabled ? 'pointer-events-none' : ''}"
on:click={() => {
if(!item.disabled){
if(item.items != null) return;
hide();
item.action();
}
}}>
<div class="w-[20px] ml-1 shrink-0">
{#if item.icon}
<img src="{item.icon}"
class="w-[17px] h-[17px] {item.icon_type == 'monotone' ? 'group-hover:invert' : ''}
{item.disabled && item.icon_type == 'monotone' ? 'contrast-50 invert' : ''}"
style:width="{item.icon_size}px" style:height="{item.icon_size}px" alt="">
{/if}
</div>
<div class="grow {item.font == 'bold' ? 'font-bold' : ''} {item.disabled ? 'text-slate-400' : 'group-hover:text-slate-50'}">
<p>{item.name}</p>
</div>
<div class="w-[10px]">
{#if item.items != null}
<svg class="fill-slate-900 group-hover:fill-slate-50 w-[10px] h-[10px]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M246.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-128-128c-9.2-9.2-22.9-11.9-34.9-6.9s-19.8 16.6-19.8 29.6l0 256c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l128-128z"/></svg>
{/if}
</div>
{#if item.items != null}
<!-- submenu items -->
<div class="absolute {left > screenWidth - 2*required_width ? '-left-full' : 'left-full'}
py-0.5 hidden group-hover:flex flex-col w-[180px] bg-slate-50 border-slate-200 border-2"
style:top="{top > screenHeight - 2*required_height ? `-${(item.items.length-1)*100}%` : '0'} ">
{#each item.items as item}
<div class="py-1 w-full flex flex-row items-center {item.disabled ? '' : 'hover:bg-blue-600'} relative group-sub
{item.disabled ? 'pointer-events-none' : ''}"
on:click={() => {
if(!item.disabled){
hide();
item.action();
}
}}>
<div class="w-[20px] ml-1 shrink-0">
{#if item.icon}
<img src="{item.icon}" width="17px" height="17px">
{/if}
</div>
<div class="grow {item.font == 'bold' ? 'font-bold' : ''} {item.disabled ? 'text-slate-400' : 'group-sub-hover:text-slate-50'}">
<p>{item.name}</p>
</div>
<div class="w-[10px]">
</div>
</div>
{/each}
</div>
{/if}
</div>
{/each}
</div>
{/each}
</div>

View File

@@ -0,0 +1,53 @@
<script>
import * as utils from '../../utils';
import _, { find, isEqual } from 'lodash';
import TitleBar from './TitleBar.svelte';
import Button from './Button.svelte';
export let self;
export let title = '';
export let message = '';
export let icon = '';
export let buttons = [];
export let button_align = 'right';//center, right
export function destroy(){
console.log(self);
self.$destroy();
}
</script>
<div class="z-20 dialog absolute inset-0 bg-slate-50/10 rounded-t-lg" on:click|self={(e) => {
e.target.querySelector('div').classList.add('animate-blink');
setTimeout(() => {
e.target.querySelector('div').classList.remove('animate-blink');
}, 400);
}}>
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col"
style:width="400px" style:height="150px">
<TitleBar options={{title: title, maximize_btn: false, minimize_btn: false}} on_click_close={destroy}></TitleBar>
<div class="grow p-2 bg-xp-yellow overflow-hidden flex flex-col justify-between border-t-0 border-2 border-blue-600">
<div class="grow flex flex-row text-[11px] p-2 text-slate-800">
{#if icon.length > 0}
<div class="w-8 h-8 mr-4 shrink-0 bg-contain" style:background-image="url({icon})"></div>
{/if}
<div>
{message}
</div>
</div>
<div class="flex flex-row pb-1 items-center {button_align == 'center' ? 'justify-center' : 'justify-end'}">
{#each buttons as button}
<Button title={button.name} on_click={button.action} focus={button.focus} style="margin-left:7px;margin-right:7px;"></Button>
{/each}
</div>
</div>
</div>
</div>
<svelte:options accessors={true}></svelte:options>

View File

@@ -0,0 +1,24 @@
<script>
import ProgressBar from "./ProgressBar.svelte";
import * as utils from '../../utils';
import {onMount} from 'svelte';
export let style = '';
export let increment = 5;
let progress_bar;
onMount(async () => {
while(progress_bar != null){
progress_bar.value = Math.min(progress_bar.value + increment, 100);
if(progress_bar.value >= 100){
progress_bar.value = 0;
}
await utils.sleep(500);
}
})
</script>
<div style="{style}">
<ProgressBar bind:this={progress_bar} value={10} style="width:100%;height:100%;"></ProgressBar>
</div>

View File

@@ -0,0 +1,73 @@
<script>
import { click_outside } from '../../utils'
export let menu = [
{
name: 'File',
items: [//group of items
[{name: 'Open'}],
[{name: 'Save'}, {name:'Save as'}],
[{name: 'Exit'}]
]
},
{
name: 'Edit',
items: [
[{name: 'Undo'}, {name: 'Redo'}],
[{name: 'Cut'}, {name: 'Copy'}, {name: 'Paste', disabled: true}, {name: 'Delete'}],
[{name: 'Find'}, {name: 'Find Next'}, {name: 'Replace'}, {name: 'Goto'}]
]
}
];
export let style = '';
let active = false;
function hide(){
active = false;
}
</script>
<div class="toolbar-menu flex flex-row items-center justify-evenly w-min font-MSSS z-10" style="{style}"
use:click_outside on:click_outside={() => active = false}>
{#each menu as menu_group}
<div class="text-[11px] text-slate-900 hover:bg-blue-600 hover:text-slate-50 relative group">
<div class="px-2 py-1" on:click={() => active = true}>
{menu_group.name}
</div>
{#if menu_group.items != null}
<div class="absolute w-[150px] border-slate-500 shadow hidden {active ? 'group-hover:block' : 'inactive-class'} border border-slate-200 bg-slate-50 left-0 top-[25px]">
{#each menu_group.items as item_group, group_index}
<div class="w-full border-slate-200 {group_index == menu_group.items.length - 1 ? '' : 'border-b'}">
{#each item_group as item}
<div class="py-1 w-full flex flex-row items-center {item.disabled? '' : 'hover:bg-blue-600'} relative group-sub"
on:click={() => {
if(!item.disabled){
hide();
console.log(active);
item.action();
}
}}>
<div class="w-[20px] ml-1 shrink-0">
{#if item.icon}
<img src="{item.icon}" width="17px" height="17px">
{/if}
</div>
<div class="grow text-slate-900 {item.font == 'bold' ? 'font-bold' : ''} {item.disabled ? 'text-slate-400' : 'group-sub-hover:text-slate-50'}">
<p class="line-clamp-1">{item.name}</p>
</div>
<div class="w-[10px]">
</div>
</div>
{/each}
</div>
{/each}
</div>
{/if}
</div>
{/each}
</div>

View File

@@ -0,0 +1,91 @@
<script>
import Window from './Window.svelte';
import { hardDrive, selectingItems} from '../../store';
import {my_pictures_id, my_music_id, desktop_folder} from '../../system';
import * as utils from '../../utils';
import * as finder from '../../finder'
import _, { find, isEqual, toLower } from 'lodash';
import TitleBar from './TitleBar.svelte';
import Viewer2 from './Viewer2.svelte';
export let self;
export let selected_items = [];
export let viewer;
export let filetypes = [];
export let filetypes_desc = 'All Files';
export let multiple = true;
let left_side_places = [
{
id: desktop_folder,
name: 'Desktop',
icon: '/images/xp/icons/Desktop.png'
},
{
id: my_pictures_id,
name: 'My Pictures',
icon: '/images/xp/icons/MyPictures.png'
},
{
id: my_music_id,
name: 'My Music',
icon: '/images/xp/icons/MyMusic.png'
},
{
id: null,
name: 'My Computer',
icon: '/images/xp/icons/MyComputer.png'
}
]
export function destroy(){
console.log(self);
self.$destroy();
}
export let on_open = () => {}
</script>
<div class="absolute inset-0 bg-slate-50/40 rounded-t-lg z-10" on:click|self={(e) => {
e.target.querySelector('div').classList.add('animate-blink');
setTimeout(() => {
e.target.querySelector('div').classList.remove('animate-blink');
}, 400);
}}>
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col"
style:width="600px" style:height="500px">
<TitleBar options={{title: 'Open', maximize_btn: false, minimize_btn: false}} on_click_close={destroy}></TitleBar>
<div class="grow p-2 pb-1 bg-xp-yellow overflow-hidden flex flex-row shadow-lg border-t-0 border-2 border-blue-600">
<div class="shrink-0 pt-1 pr-1 w-[100px]">
<div class="h-7 mr-2 flex flex-row justify-end items-center">
<span class="text-[11px] text-black">Look in:</span>
</div>
<div class="bg-xp-yellow-light shadow rounded w-full">
{#each left_side_places as place}
<div class="w-full h-[80px] flex flex-col items-center p-2 hover:bg-slate-100 rounded"
on:click={() => viewer.open(place.id)}>
<div class="w-8 h-8 bg-contain bg-no-repeat" style:background-image="url({place.icon})"></div>
<span class="mt-1 text-[12px] text-black">{place.name}</span>
</div>
{/each}
</div>
</div>
<div class="grow flex flex-col relative">
<Viewer2 bind:this={viewer} filetypes_desc={filetypes_desc} filetypes={filetypes.map(el => el.toLowerCase())} multiple={multiple}
on_open={() => {
selected_items = viewer.selectingItems;
on_open();
}}
on_cancel={destroy}>
</Viewer2>
</div>
</div>
</div>
</div>
<svelte:options accessors={true}></svelte:options>

View File

@@ -0,0 +1,41 @@
<script>
import * as fs from '../../fs';
import {onMount} from 'svelte';
export let default_icon;
export let fs_id;
let preview_url;
let node_ref;
let observer;
onMount(async () => {
observer = new IntersectionObserver(intersect_callback, {
root: null,
threshold: 1
})
observer.observe(node_ref);
})
let intersect_callback = (entries, observer) => {
entries.forEach((entry) => {
let { target, isIntersecting } = entry;
if(isIntersecting){
load_preview();
}
});
};
async function load_preview(){
if(preview_url != null) return;
if(fs_id == null) return;
let url = await fs.get_url(fs_id);
// console.log('load', fs_id, 'with url', url);
preview_url = `url(${url})`;
}
</script>
<div bind:this={node_ref} class="w-[50px] h-[50px] shrink-0 bg-contain bg-no-repeat bg-center"
style:background-image="{preview_url || default_icon}">
</div>

View File

@@ -0,0 +1,43 @@
<script>
import { zIndex, runningPrograms, contextMenu } from '../../store';
let node_ref;
export let program;
function on_rightclick(ev){
contextMenu.set(null);
console.log(program);
console.log({maximized: program.window.maximized, minimized: program.window.minimized})
contextMenu.set({x: ev.x, y: ev.y, type: 'ProgramTile', originator: program});
}
function on_mousedown(ev){
// program.window.focus();
}
function on_click(ev){
if(!program.window.minimized){
if(program.window.z_index == $zIndex){
console.log('minimize');
program.window.on_click_minimize();
} else {
program.window.focus();
}
} else {
program.window.restore();
}
}
</script>
<div bind:this={node_ref} on:contextmenu={on_rightclick} on:mousedown={on_mousedown} on:click={on_click}
program-id="{program.id}"
class="program-tile h-full w-[150px] min-w-[70px] flex flex-row items-center max-w-[200px] overflow-hidden rounded-sm hover:brightness-125"
style:background="{program.window.z_index == $zIndex? 'rgb(30,82,183)' : 'rgb(60,129,243)'}"
style:box-shadow="{program.window.z_index == $zIndex? 'rgb(0 0 0 / 20%) 0px 0px 1px 1px inset, rgb(0 0 0 / 70%) 1px 0px 1px inset' : 'rgb(0 0 0 / 30%) -1px 0px inset, rgb(255 255 255 / 20%) 1px 1px 1px inset'}"
>
<img src="{program.options.icon}" width="15px" height="15px" class="shrink-0 ml-2 pointer-events-none" alt="">
<p class="text-[11px] text-slate-50 text-ellipsis mx-1 line-clamp-1 leading-none">{program.options.title}</p>
</div>

View File

@@ -0,0 +1,13 @@
<script>
export let value = 0;
export let total = 100;
export let style = '';
</script>
<div class="bg-slate-100 border border-slate-500 rounded-sm w-10 h-1 p-0.5 overflow-hidden" style="{style}">
<div style:width="{100*value/total}%"
class="h-full bg-[url(/images/xp/battery_cell.png)] bg-contain bg-repeat-x">
</div>
</div>
<svelte:options accessors={true}></svelte:options>

View File

@@ -0,0 +1,31 @@
<script>
import {tooltip} from './tooltip';
export let title = '';
export let icon = '';
export let style = '';
export let tooltip_message = '';
export let disabled = false;
export let expandable = false;
export let on_click = () => {
}
</script>
<div class="p-2 h-min flex flex-row bg-xp-yellow rounded items-center {!disabled ? 'hover:brightness-105 hover:shadow' : ''}"
use:tooltip tooltip="{tooltip_message}"
on:click={() => {
if(!disabled){
on_click();
}
}}>
<div class="h-[25px] w-[25px] bg-contain bg-no-repeat {disabled? 'grayscale' : ''}" style:background-image="url({icon})">
</div>
{#if title.trim() != ''}
<div class="text-[12px] text-slate-900 ml-2">{title}</div>
{/if}
{#if expandable}
<div class="w-[10px] ml-1">
<svg class="{disabled ? 'fill-gray-400' : 'fill-slate-700'} group-hover:fill-slate-50 w-[10px] h-[10px]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M137.4 374.6c12.5 12.5 32.8 12.5 45.3 0l128-128c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8L32 192c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l128 128z"/></svg>
</div>
{/if}
</div>

View File

@@ -0,0 +1,20 @@
<script>
export let checked = false;
export let label = '';
export let size = 15;
export let in_progress = false;
</script>
<div class="flex flex-row">
<div class="group bg-[linear-gradient(135deg,#dcdcd7,#fff)] shrink-0 rounded-full border border-[#1d5281] relative"
style:width='{size}px' style:height='{size}px'>
<div class="{in_progress ? 'bg-gradient-to-r' : ''} group-hover:bg-gradient-to-r from-orange-300 to-orange-200 absolute inset-0 rounded-full p-[2px]">
<div class="w-full h-full bg-[linear-gradient(135deg,#dcdcd7,#fff)] opacity-70 rounded-full"></div>
</div>
{#if checked}
<div class="bg-[url(/images/xp/radio_check.png)] bg-cover absolute inset-1/4"></div>
{/if}
</div>
<div class="ml-2 leading-none {in_progress ? 'text-orange-400' : ''}">{label}</div>
</div>
<svelte:options accessors={true}></svelte:options>

View File

@@ -0,0 +1,33 @@
<script>
import { hardDrive, queueProgram, contextMenu } from '../../store';
import { recycle_bin_id} from '../../system';
export let style;
export let classes;
$: icon = $hardDrive[recycle_bin_id]?.files.length > 0 || $hardDrive[recycle_bin_id]?.folders.length > 0 ?
'url(/images/xp/icons/RecycleBinfull.png)' : 'url(/images/xp/icons/RecycleBinempty.png)';
function on_dbclick(){
let fs_item = $hardDrive[recycle_bin_id];
queueProgram.set({
path: './programs/my_computer.svelte',
fs_item: fs_item
})
}
function on_rightclick(ev){
contextMenu.set(null);
contextMenu.set({x: ev.x, y: ev.y, type: 'RecycleBin', originator: null});
}
</script>
<div class="flex flex-col items-center absolute bottom-2 right-2 {classes}"
style="{style}"
on:dblclick={on_dbclick}
on:contextmenu={on_rightclick}
>
<div class="w-[40px] h-[40px] bg-contain" style:background-image="{icon}"></div>
<p class="text-center text-[11px] font-MSSS text-white" style="text-shadow: 1px 1px 2px black;">Recycle Bin</p>
</div>

View File

@@ -0,0 +1,97 @@
<script>
import Window from './Window.svelte';
import { hardDrive, selectingItems} from '../../store';
import {my_pictures_id, my_music_id, desktop_folder} from '../../system';
import * as utils from '../../utils';
import * as finder from '../../finder'
import _, { find, isEqual } from 'lodash';
import TitleBar from './TitleBar.svelte';
import Viewer3 from './Viewer3.svelte';
export let self;
export let id;
export let viewer;
export let filetypes = [];
export let selected_filetype;
export let filename;
export let parent_id;
let left_side_places = [
{
id: desktop_folder,
name: 'Desktop',
icon: '/images/xp/icons/Desktop.png'
},
{
id: my_pictures_id,
name: 'My Pictures',
icon: '/images/xp/icons/MyPictures.png'
},
{
id: my_music_id,
name: 'My Music',
icon: '/images/xp/icons/MyMusic.png'
},
{
id: null,
name: 'My Computer',
icon: '/images/xp/icons/MyComputer.png'
}
]
export function destroy(){
console.log(self);
self.$destroy();
}
export let on_save = () => {}
</script>
<div class="absolute inset-0 bg-slate-50/40 rounded-t-lg z-10" on:click|self={(e) => {
e.target.querySelector('div').classList.add('animate-blink');
setTimeout(() => {
e.target.querySelector('div').classList.remove('animate-blink');
}, 400);
}}>
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col"
style:width="600px" style:height="500px">
<TitleBar options={{title: 'Save', maximize_btn: false, minimize_btn: false}} on_click_close={destroy}></TitleBar>
<div class="absolute inset-0 top-[28px] bg-xp-yellow shadow-lg border-t-0 border-2 border-blue-600">
<div class="absolute top-1 left-1 bottom-0 w-[100px]">
<div class="h-7 mr-2 flex flex-row justify-end items-center">
<span class="text-[11px] text-black">Look in:</span>
</div>
<div class="bg-xp-yellow-light shadow rounded w-full">
{#each left_side_places as place}
<div class="w-full h-[80px] flex flex-col items-center p-2 hover:bg-slate-100 rounded"
on:click={() => viewer.open(place.id)}>
<div class="w-8 h-8 bg-contain bg-no-repeat" style:background-image="url({place.icon})"></div>
<span class="mt-1 text-[12px] text-black">{place.name}</span>
</div>
{/each}
</div>
</div>
<div class="absolute top-1 left-[110px] right-1 bottom-1">
<Viewer3 bind:this={viewer} id={id} filetypes={filetypes} selected_filetype={selected_filetype}
on_save={() => {
parent_id = viewer.id;
filename = viewer.filename;
selected_filetype = viewer.select_box.items[viewer.select_box.selected_index];
console.log(selected_filetype);
on_save();
}}
on_cancel={destroy}>
</Viewer3>
</div>
</div>
</div>
</div>
<svelte:options accessors={true}></svelte:options>

View File

@@ -0,0 +1,73 @@
<script>
import * as utils from '../../utils';
const {click_outside} = utils;
import {onMount} from 'svelte';
export let items = []
export let selected_index = 0;
export let style = '';
export let direction;
let item_height = 24;
let expand = false;
let node_ref;
let dropbox_pos = '';
function on_click_expand(){
cal_dropbox_pos();
expand = true;
console.log({expand});
}
function cal_dropbox_pos(){
if(direction == 'bottom'){
dropbox_pos = 'top:100%;'
} else if(direction == 'top'){
dropbox_pos = 'bottom:100%;'
} else {
if(document.body.offsetHeight - node_ref.getBoundingClientRect().y > 150){
dropbox_pos = 'top:100%;'
} else {
dropbox_pos = 'bottom:100%;'
}
}
}
</script>
<div bind:this={node_ref} class="bg-slate-50 h-6 text-slate-800 border border-blue-300 p-1 text-[11px] absolute" style="{style}"
use:click_outside on:click_outside={()=> expand = false}>
<div class="absolute bg-slate-50 w-full left-0 max-h-[100px] overflow-y-auto
box-content border border-slate-300 {expand ? 'block' : 'hidden'}"
style:height="{expand ? Math.min(items.length*item_height, 100) : 0}px" style="{dropbox_pos}">
{#each items as item, index}
<div class="box-border w-full flex flex-row p-0.5 pl-2 items-center hover:bg-blue-600 hover:text-slate-50
{index != 0 ? 'border-t' : ''} border-slate-300"
style:height="{item_height}px"
on:click={() => {
selected_index = index;
expand = false;
}}>
{item.name}
</div>
{/each}
</div>
<div class="absolute inset-0 flex flex-row items-center pl-2"
on:click={on_click_expand}>
{items[selected_index]?.name}
</div>
{#if items.length > 1}
<div class="absolute right-[1px] top-[1px] bottom-[1px] p-1 rounded bg-blue-200 border-2 border-slate-50 box-border"
style:width="{item_height-2}px" on:click={on_click_expand}>
<svg class="w-full h-full fill-blue-700" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"/>
</svg>
</div>
{/if}
</div>
<svelte:options accessors={true}></svelte:options>

View File

@@ -0,0 +1,46 @@
<script>
export let items = [];
export let selected;
export let size = 'sm';
export let style = 'margin-left:2px;';
</script>
<div class="w-min shrink-0 flex flex-row items-center justify-evenly" style="{style}">
{#each items as item}
<div on:click={() => selected = item}
class="grow text-slate-800 tab-item {selected == item ? 'selected' : ''}
{size == 'sm' ? 'text-[11px]' : ''} {size == 'md' ? 'text-base' : ''} {size == 'lg' ? 'text-lg' : ''}">
<span>{item}</span>
</div>
{/each}
</div>
<style>
.tab-item {
background: linear-gradient(180deg,#fff,#fafaf9 26%,#f0f0ea 95%,#ecebe5);
margin-left: -1px;
margin-right: 2px;
border-radius: 0;
border-color: #91a7b4;
border-top-right-radius: 3px;
border-top-left-radius: 3px;
padding: 0 12px 3px;
text-align: center;
}
.tab-item:hover {
border-top: 1px solid #e68b2c;
box-shadow: inset 0 2px #ffc73c;
}
.tab-item.selected {
background: #fcfcfe;
border-color: #919b9c;
margin-right: 0px;
border-bottom: 1px solid transparent;
border-top: 1px solid #e68b2c;
box-shadow: inset 0 2px #ffc73c;
}
</style>

View File

@@ -0,0 +1,60 @@
<script>
export let options = {};
let {close_btn=true, maximize_btn=true, minimize_btn=true,
close_btn_disabled=false, maximize_btn_disabled=false, minimize_btn_disabled=false} = options;
export let inactive = false;
export let maximized = false;
export let on_click_maximize = () => {}
export let on_click_minimize = () => {}
export let on_click_close = () => {}
export function update_icon(icon){
options.icon = icon;
}
export function update_title(title){
options.title = title;
}
</script>
<div class="titlebar shrink-0 flex rounded-tl-lg rounded-tr-lg items-center justify-between h-7 p-1 font-Trebuchet
{inactive ? 'bg-[linear-gradient(var(--titlebar-gradient-inactive))]' : 'bg-[linear-gradient(var(--titlebar-gradient))]'}">
{#if options.icon}
<img src="{options.icon}" width="20px" height="20px" class="ml-1" alt="">
{/if}
<p class="text-white font-semibold mr-4 text-[12px] grow ml-1 leading-tight line-clamp-1 text-ellipsis">{options.title}</p>
<div class="flex mr-0.5 shrink-0">
{#if minimize_btn}
<button disabled={minimize_btn_disabled} on:click={on_click_minimize}
class="group w-5 h-5 ml-1 group">
<img src="/images/xp/icons/Minimize.png" class="w-full h-full {minimize_btn_disabled ? 'contrast-75' : 'group-hover:brightness-110'}" >
</button>
{/if}
{#if maximize_btn}
<button disabled={maximize_btn_disabled} on:click={on_click_maximize}
class="group w-5 h-5 ml-1 group" >
{#if maximized}
<img src="/images/xp/icons/Restore.png" class="w-full h-full {maximize_btn_disabled ? 'contrast-75' : 'group-hover:brightness-110'}" >
{:else}
<img src="/images/xp/icons/Maximize.png" class="w-full h-full {maximize_btn_disabled ? 'contrast-75' : 'group-hover:brightness-110'}" >
{/if}
</button>
{/if}
{#if close_btn}
<button disabled={close_btn_disabled} on:click={on_click_close}
class="group w-5 h-5 ml-1 group">
<img src="/images/xp/icons/Exit.png" class="w-full h-full {close_btn_disabled ? 'contrast-75' : 'group-hover:brightness-110'}" >
</button>
{/if}
</div>
</div>
<svelte:options accessors={true}></svelte:options>

View File

@@ -0,0 +1,29 @@
<script>
import {tooltip} from './tooltip';
export let style = '';
export let tooltip_message;
export let icon;
export let on_click = () => {
console.log('on click')
}
function on_mouseenter(e){
if(tooltip_message == null) return;
let position = {left: e.x, top: e.y - 40};
if(tooltip_message.length >= 20){
position.top = position.top - 20;
}
tooltip.set({message: tooltip_message, position});
}
function on_mouseleave(e){
tooltip.set(null);
}
</script>
<div class="mr-1 w-4 h-4 bg-no-repeat bg-contain" style:background-image="url({icon})"
on:click={on_click} use:tooltip tooltip="{tooltip_message}">
</div>

View File

@@ -0,0 +1,250 @@
<script>
import { contextMenu, hardDrive } from '../../store'
import {my_computer} from '../../../lib/system';
import * as finder from '../../finder';
import * as utils from '../../utils';
import { doctypes, icons, hidden_items } from '../../system';
import * as fs from '../../fs';
const {click_outside} = utils;
import { createEventDispatcher, onMount, tick } from 'svelte';
import short from 'short-uuid';
import {get, set} from 'idb-keyval';
import { filter, map } from 'lodash';
import Button from './Button.svelte';
let dispatch = createEventDispatcher();
let history = [null];
let page_index = 0;
$: url = finder.to_url(history[page_index]) || 'My Computer';
export let id;
$: folders = $hardDrive[id] == null ? [] : $hardDrive[id].folders.map(id => $hardDrive[id]);
$: files = $hardDrive[id] == null ? [] : $hardDrive[id].files.map(id => $hardDrive[id]);
$: items = [...files, ...folders]
.filter(el => el != null)
.filter(el => !hidden_items.includes(el.id));
let computer = my_computer.map(el => $hardDrive[el]);
export let selectingItems = [];
export let filetypes_desc;
export let filetypes = [];
export let multiple = true;
function on_click(ev, item){
if(!is_desired(item)) return;
if(item.type != 'file') return;
let selected = selectingItems.includes(item.id);
if((ev.ctrlKey || ev.metaKey) && multiple){
console.log('ctrl key pressed');
if(selected){
selectingItems = selectingItems.filter(el => el != item.id);
} else {
selectingItems = [...selectingItems, item.id];
}
} else {
selectingItems = selected ? [] : [item.id];
}
}
function clear_selection(){
console.log(selectingItems);
console.log('clear_selection');
selectingItems = [];
}
function is_desired(item){
if(item.type != 'file') return true;
if(filetypes.length == 0) return true;
return filetypes.includes(item.ext);
}
export function open(item_id){
clear_selection();
let fs_item = $hardDrive[item_id];
if(fs_item?.type == 'file'){
selectingItems = [item_id];
on_open();
} else {
history = [...history.slice(0, page_index+1), item_id];
page_index = history.length - 1;
id = history[page_index];
}
}
function back(){
page_index = Math.max(0, page_index - 1);
id = history[page_index];
}
function next(){
page_index = Math.min(history.length - 1, page_index + 1);
id = history[page_index];
}
function up(){
let parent_id = $hardDrive[history[page_index]].parent;
open(parent_id);
}
function file_icon(item){
if(item == null) return null;
if(item.icon != null){
return `url(${item.icon})`
}
if(icons[item.ext] != null){
return `url(/images/xp/icons/${icons[item.ext]})`
}
return null;
}
function on_user_input(e){
if(e.key == 'Enter'){
let id = finder.to_id(e.target.value);
if(id == null){
id = finder.to_id_nocase(e.target.value);
}
console.log('found id', id);
if(id){
open(id);
e.target.blur();
}
}
}
export let on_open = () => {}
export let on_cancel = () => {}
</script>
<div class="absolute inset-0 overflow-auto bg-xp-yellow flex flex-col"
use:click_outside on:click_outside={() => clear_selection()}>
<div class="h-6 mb-2 flex flex-row items-center text-[11px]">
<div class="h-full w-[300px] relative">
<input class="absolute inset-0 w-[300px] pl-7 border border-blue-300 outline-none" type="text"
on:click={(e) => e.target.select()} on:keyup={on_user_input} value="{url}">
<div class="w-[17px] h-[17px] absolute top-[4px] left-[4px] bg-no-repeat
{id == null ? 'bg-[url(/images/xp/icons/MyComputer.png)]' : 'bg-[url(/images/xp/icons/FolderClosed.png)]'} bg-contain"
style:background-image="{file_icon($hardDrive[id])}">
</div>
</div>
<button class="mx-1.5 ml-4 w-4 h-4 bg-[url(/images/xp/icons/Back.png)] bg-contain disabled:grayscale"
disabled={page_index == 0}
on:click={back}>
</button>
<button class="mx-1.5 w-4 h-4 bg-[url(/images/xp/icons/Forward.png)] bg-contain disabled:grayscale"
disabled={page_index == history.length-1}
on:click={next}>
</button>
<button class="mx-1.5 w-5 h-5 bg-[url(/images/xp/icons/Up.png)] bg-contain disabled:grayscale"
disabled={history[page_index] == null}
on:click={up}>
</button>
</div>
<div class="w-full bg-slate-50 grow border overflow-auto border-blue-300" class:hidden={id == null}
on:click|self={clear_selection}>
{#each items as item}
<div fs-id="{item.id}" class="w-[100px] overflow-hidden m-2 inline-flex flex-row items-center font-MSSS relative
{is_desired(item) ? '' : 'opacity-50'}"
on:dblclick={() => open(item.id)}
on:click={(e) => on_click(e, item)}>
<div class="w-[30px] h-[30px] shrink-0 bg-contain bg-no-repeat
{item.type == 'folder' ? 'bg-[url(/images/xp/icons/FolderClosed.png)]' : 'bg-[url(/images/xp/icons/Default.png)]'} "
style:background-image="{file_icon(item)}">
</div>
<p class="px-1 text-[11px] break-words line-clamp-2 text-ellipsis leading-tight
{selectingItems?.includes(item.id) ? 'bg-blue-600 text-slate-50' : ''}">
{item.name}
</p>
</div>
{/each}
</div>
<div class="w-full bg-slate-50 grow border overflow-auto border-blue-300" class:hidden={id != null}>
<p class="ml-2 mt-0.5 font-MSSS text-black text-[11px] font-bold">Files Stored on This Computer</p>
<div class="mb-4 w-[300px] h-[2px] bg-gradient-to-r from-blue-500 to-slate-50"></div>
{#each computer.filter(el => el.type == 'folder') as item}
<div class="w-[150px] ml-4 mr-8 overflow-hidden inline-flex flex-row items-center font-MSSS"
on:dblclick={() => open(item.id)}>
<div class="w-[40px] h-[40px] shrink-0 bg-[url(/images/xp/icons/FolderClosed.png)] bg-contain"
style:background-image="{item.icon == null ? '' : `url(${item.icon})`}">
</div>
<div class="px-1 text-[11px] line-clamp-2 text-ellipsis leading-tight">
{item.display_name != null ? item.display_name : item.name}
</div>
</div>
{/each}
<p class="ml-2 mt-4 font-MSSS text-black text-[11px] font-bold">Hard Disk Drives</p>
<div class="mb-4 w-[300px] h-[2px] bg-gradient-to-r from-blue-500 to-slate-50"></div>
{#each computer.filter(el => el.type == 'drive') as item}
<div class="w-[150px] ml-4 mr-8 overflow-hidden inline-flex flex-row items-center font-MSSS"
on:dblclick={() => open(item.id)}>
<div class="w-[50px] h-[50px] shrink-0 bg-[url(/images/xp/icons/LocalDisk.png)] bg-contain">
</div>
<div class="px-1 text-[11px] line-clamp-2 text-ellipsis leading-tight">
{item.display_name != null ? item.display_name : item.name}
</div>
</div>
{/each}
<p class="ml-2 mt-4 font-MSSS text-black text-[11px] font-bold">Devices with Removable Storage</p>
<div class="mb-4 w-[300px] h-[2px] bg-gradient-to-r from-blue-500 to-slate-50"></div>
{#each computer.filter(el => el.type == 'removable_storage') as item}
<div class="w-[150px] ml-4 mr-8 overflow-hidden inline-flex flex-row items-center font-MSSS"
on:dblclick={() => open(item.id)}>
<div class="w-[50px] h-[50px] shrink-0 bg-[url(/images/xp/icons/RemovableMedia.png)] bg-contain">
</div>
<div class="px-1 text-[11px] line-clamp-2 text-ellipsis leading-tight">
{item.display_name != null ? item.display_name : item.name}
</div>
</div>
{/each}
</div>
<div class="shrink-0 w-full h-[70px] text-[11px] text-black">
<div class="w-full flex flex-row items-center my-2">
<div class="w-[100px] shrink-0">File name:</div>
<div class="grow">
<input disabled type="text"
value="{selectingItems.map(el => $hardDrive[el].name).join(', ')}"
class="w-full h-6 text-[11px] outline-none border border-blue-300 disabled:bg-slate-50">
</div>
<div class="w-[100px] shrink-0 flex flex-row justify-end ">
<Button title="Open" on_click={on_open} disabled={selectingItems.length == 0}></Button>
</div>
</div>
<div class="w-full flex flex-row items-center my-2">
<div class="w-[100px] shrink-0">Files of type:</div>
<div class="grow">
<input disabled type="text"
value="{filetypes_desc} ({filetypes.join(', ')})"
class="w-full h-6 p-0.5 text-[11px] outline-none border border-blue-300 disabled:bg-slate-50">
</div>
<div class="w-[100px] shrink-0 flex flex-row justify-end ">
<Button title="Cancel" on_click={on_cancel}></Button>
</div>
</div>
</div>
</div>
<svelte:options accessors={true}></svelte:options>

View File

@@ -0,0 +1,223 @@
<script>
import { contextMenu, hardDrive } from '../../store'
import {my_computer} from '../../../lib/system';
import * as finder from '../../finder';
import * as utils from '../../utils';
import { doctypes, icons, hidden_items } from '../../system';
import * as fs from '../../fs';
const {click_outside} = utils;
import { createEventDispatcher, onMount, tick } from 'svelte';
import short from 'short-uuid';
import {get, set} from 'idb-keyval';
import { filter, indexOf, map } from 'lodash';
import Button from './Button.svelte';
import SelectBox from './SelectBox.svelte';
let dispatch = createEventDispatcher();
export let id;
let history = [id];
let page_index = 0;
$: url = finder.to_url(history[page_index]) || 'My Computer';
$: folders = $hardDrive[id] == null ? [] : $hardDrive[id].folders.map(id => $hardDrive[id]);
$: files = $hardDrive[id] == null ? [] : $hardDrive[id].files.map(id => $hardDrive[id]);
$: items = [...files, ...folders]
.filter(el => el != null)
.filter(el => !hidden_items.includes(el.id));
let computer = my_computer.map(el => $hardDrive[el]);
export let selected_filetype;
export let filetypes = [];
export let filename = '';
export let select_box;
function is_desired(item){
return true;
}
export function open(item_id){
let fs_item = $hardDrive[item_id];
if(fs_item?.type == 'file'){
} else {
history = [...history.slice(0, page_index+1), item_id];
page_index = history.length - 1;
id = history[page_index];
}
}
function back(){
page_index = Math.max(0, page_index - 1);
id = history[page_index];
}
function next(){
page_index = Math.min(history.length - 1, page_index + 1);
id = history[page_index];
}
function up(){
let parent_id = $hardDrive[history[page_index]].parent;
open(parent_id);
}
function file_icon(item){
if(item == null) return null;
if(item.icon != null){
return `url(${item.icon})`
}
if(icons[item.ext] != null){
return `url(/images/xp/icons/${icons[item.ext]})`
}
return null;
}
function on_user_input(e){
if(e.key == 'Enter'){
let id = finder.to_id(e.target.value);
if(id == null){
id = finder.to_id_nocase(e.target.value);
}
console.log('found id', id);
if(id){
open(id);
e.target.blur();
}
}
}
export let on_save = () => {}
export let on_cancel = () => {}
</script>
<div class="absolute inset-0 bg-xp-yellow">
<div class="absolute inset-1 top-0.5 h-6 mb-2 flex flex-row items-center text-[11px]">
<div class="h-full w-[300px] relative">
<input class="absolute inset-0 w-[300px] pl-7 border border-blue-300 outline-none" type="text"
on:click={(e) => e.target.select()} on:keyup={on_user_input} value="{url}">
<div class="w-[17px] h-[17px] absolute top-[4px] left-[4px] bg-no-repeat
{id == null ? 'bg-[url(/images/xp/icons/MyComputer.png)]' : 'bg-[url(/images/xp/icons/FolderClosed.png)]'} bg-contain"
style:background-image="{file_icon($hardDrive[id])}">
</div>
</div>
<button class="mx-1.5 ml-4 w-4 h-4 bg-[url(/images/xp/icons/Back.png)] bg-contain disabled:grayscale"
disabled={page_index == 0}
on:click={back}>
</button>
<button class="mx-1.5 w-4 h-4 bg-[url(/images/xp/icons/Forward.png)] bg-contain disabled:grayscale"
disabled={page_index == history.length-1}
on:click={next}>
</button>
<button class="mx-1.5 w-5 h-5 bg-[url(/images/xp/icons/Up.png)] bg-contain disabled:grayscale"
disabled={history[page_index] == null}
on:click={up}>
</button>
</div>
<div class="absolute top-7 left-1 right-1 h-[360px] overflow-auto bg-slate-50 border border-blue-300" class:hidden={id == null}>
{#each items as item}
<div fs-id="{item.id}" class="w-[100px] overflow-hidden m-2 inline-flex flex-row items-center font-MSSS relative
{is_desired(item) ? '' : 'opacity-50'}"
on:dblclick={() => open(item.id)}>
<div class="w-[30px] h-[30px] shrink-0 bg-contain bg-no-repeat
{item.type == 'folder' ? 'bg-[url(/images/xp/icons/FolderClosed.png)]' : 'bg-[url(/images/xp/icons/Default.png)]'} "
style:background-image="{file_icon(item)}">
</div>
<p class="px-1 text-[11px] break-words line-clamp-2 text-ellipsis leading-tight">
{item.name}
</p>
</div>
{/each}
</div>
<div class="absolute top-7 left-1 right-1 h-[360px] overflow-auto bg-slate-50 border border-blue-300" class:hidden={id != null}>
<p class="ml-2 mt-0.5 font-MSSS text-black text-[11px] font-bold">Files Stored on This Computer</p>
<div class="mb-4 w-[300px] h-[2px] bg-gradient-to-r from-blue-500 to-slate-50"></div>
{#each computer.filter(el => el.type == 'folder') as item}
<div class="w-[150px] ml-4 mr-8 overflow-hidden inline-flex flex-row items-center font-MSSS"
on:dblclick={() => open(item.id)}>
<div class="w-[40px] h-[40px] shrink-0 bg-[url(/images/xp/icons/FolderClosed.png)] bg-contain"
style:background-image="{item.icon == null ? '' : `url(${item.icon})`}">
</div>
<div class="px-1 text-[11px] line-clamp-2 text-ellipsis leading-tight">
{item.display_name != null ? item.display_name : item.name}
</div>
</div>
{/each}
<p class="ml-2 mt-4 font-MSSS text-black text-[11px] font-bold">Hard Disk Drives</p>
<div class="mb-4 w-[300px] h-[2px] bg-gradient-to-r from-blue-500 to-slate-50"></div>
{#each computer.filter(el => el.type == 'drive') as item}
<div class="w-[150px] ml-4 mr-8 overflow-hidden inline-flex flex-row items-center font-MSSS"
on:dblclick={() => open(item.id)}>
<div class="w-[50px] h-[50px] shrink-0 bg-[url(/images/xp/icons/LocalDisk.png)] bg-contain">
</div>
<div class="px-1 text-[11px] line-clamp-2 text-ellipsis leading-tight">
{item.display_name != null ? item.display_name : item.name}
</div>
</div>
{/each}
<p class="ml-2 mt-4 font-MSSS text-black text-[11px] font-bold">Devices with Removable Storage</p>
<div class="mb-4 w-[300px] h-[2px] bg-gradient-to-r from-blue-500 to-slate-50"></div>
{#each computer.filter(el => el.type == 'removable_storage') as item}
<div class="w-[150px] ml-4 mr-8 overflow-hidden inline-flex flex-row items-center font-MSSS"
on:dblclick={() => open(item.id)}>
<div class="w-[50px] h-[50px] shrink-0 bg-[url(/images/xp/icons/RemovableMedia.png)] bg-contain">
</div>
<div class="px-1 text-[11px] line-clamp-2 text-ellipsis leading-tight">
{item.display_name != null ? item.display_name : item.name}
</div>
</div>
{/each}
</div>
<div class="absolute bottom-1 right-1 left-1 h-[70px] text-[11px] text-black">
<div class="absolute top-0 right-0 left-0 h-[35px] flex flex-row items-center">
<div class="w-[100px] shrink-0">File name:</div>
<div class="grow">
<input type="text" bind:value={filename}
on:keyup={(e) => {
if(e.key == 'Enter' && id != null && filename.length > 0){
on_save();
}
}}
class="w-full h-6 text-[11px] outline-none border border-blue-300 disabled:bg-slate-50">
</div>
<div class="w-[100px] shrink-0 flex flex-row justify-end ">
<Button title="Save" on_click={on_save} disabled={filename.length == 0 || id == null}></Button>
</div>
</div>
<div class="absolute bottom-0 right-0 left-0 h-[35px] flex flex-row items-center justify-between">
<div class="w-[100px] shrink-0">Save as type:</div>
<SelectBox
style="left:100px;right:100px;bottom:5px;"
bind:this={select_box} items={filetypes}
selected_index={filetypes.indexOf(selected_filetype) >= 0 ? filetypes.indexOf(selected_filetype) : 0}>
</SelectBox>
<div class="w-[100px] shrink-0 flex flex-row justify-end ">
<Button title="Cancel" on_click={on_cancel}></Button>
</div>
</div>
</div>
</div>
<svelte:options accessors={true}></svelte:options>

View File

@@ -0,0 +1,258 @@
<script>
import {onMount} from 'svelte';
import TitleBar from './TitleBar.svelte';
import * as utils from '../../utils';
const {click_outside} = utils;
import _ from 'lodash';
import {get, set} from 'idb-keyval';
import { zIndex, runningPrograms } from '../../store';
export let options = {};
let titlebar;
export let node_ref;
let saved_position;
export let maximized;
export let minimized;
let translateX = '';
let translateY = '';
let animation_enabled = false;
export let on_focused = () => {
}
export let z_index = 0;
onMount(async () => {
if(options.exec_path != null){
let rect = await get(options.exec_path);
console.log(rect);
if(rect){
let workspace = document.querySelector('#work-space');
if(rect.left + rect.width <= workspace.offsetWidth
&& rect.top + rect.height <= workspace.offsetHeight){
options.top = rect.top;
options.left = rect.left;
options.width = rect.width;
options.height = rect.height;
}
}
}
if(options.top == null){
options.top = (node_ref.parentNode.offsetHeight - node_ref.offsetHeight)/2;
}
if(options.left == null){
options.left = (node_ref.parentNode.offsetWidth - node_ref.offsetWidth)/2;
}
set_position({top: options.top, left: options.left, width: node_ref.width, height: node_ref.height});
if(options.resizable == null){
options.resizable = true;
}
if(options.draggable == null){
options.draggable = true;
}
setup_gestures();
zIndex.update(value => value + 1);
z_index = $zIndex;
node_ref.style.removeProperty('opacity');
setTimeout(() => {
animation_enabled = true;
}, 500)
})
export let on_click_close = () => {
}
export let on_click_maximize = () => {
if(!options.resizable) return;
minimized = false;
if(maximized){
set_position(saved_position);
maximized = false;
} else {
// let rect = utils.relative_rect(node_ref.parentNode.getBoundingClientRect(), node_ref.getBoundingClientRect());
// console.log(rect);
saved_position = {top: node_ref.offsetTop, left: node_ref.offsetLeft, width: node_ref.offsetWidth, height: node_ref.offsetHeight};
set_position({top: 0, left: 0, width: node_ref.parentNode.offsetWidth, height: node_ref.parentNode.offsetHeight});
maximized = true;
}
focus();
}
export let on_click_minimize = () => {
let window_center = get_center_point(node_ref.getBoundingClientRect());
let tile_center = get_center_point(document.querySelector(`.program-tile[program-id="${options.id}"]`)?.getBoundingClientRect());
translateX = `translateX(${tile_center.x-window_center.x}px)`;
translateY = `translateY(${tile_center.y-window_center.y}px)`;
console.log(`${translateX} ${translateY} scale(0.1)`);
minimized = true;
loose_focus();
}
export function restore(){
if(minimized){
minimized = false;
} else if(maximized) {
on_click_maximize();
}
focus();
}
export function focus(){
if(z_index != $zIndex){
zIndex.update(value => value + 1);
z_index = $zIndex;
on_focused();
}
}
export function loose_focus(){
if(z_index == $zIndex){
console.log('loose focus');
zIndex.update(value => value + 1);
}
}
function get_center_point(rect){
if(rect == null){
return {x: document.body.offsetWidth*0.5, y: document.body.offsetHeight*0.5}
}
return {x: rect.x + rect.width*0.5, y: rect.y+rect.height*0.5}
}
export function set_position({top, left, width, height}){
node_ref.style.top = `${top}px`;
node_ref.style.left = `${left}px`;
node_ref.style.width = `${width}px`;
node_ref.style.height = `${height}px`;
}
function setup_gestures(){
if(options.draggable){
jQuery(node_ref).draggable({
containment: 'parent',
handle: '.titlebar',
stop: async () => {
if(options.exec_path){
await set(options.exec_path, node_ref.getBoundingClientRect())
}
}
})
}
if(options.resizable){
jQuery(node_ref).resizable({
minWidth: options.min_width,
minHeight: options.min_height,
aspectRatio: options.aspect_ratio,
containment: 'parent',
handles: 'all',
classes: {
"ui-resizable-se": "ui-icon ui-icon-gripsmall-diagonal-se opacity-0"
},
stop: async () => {
if(options.exec_path){
await set(options.exec_path, node_ref.getBoundingClientRect())
}
}
})
}
}
export function update_icon(icon){
titlebar.update_icon(icon);
}
export function update_title(title){
runningPrograms.update(values => {
let program = values.find(el => el.options.id == options.id);
let index = values.indexOf(program);
if(index >= 0){
values[index].options.title = title;
}
return values;
})
titlebar.update_title(title);
}
export function show_toast({theme='dark', message}){
let toast = document.createElement('div');
toast.style.position = 'absolute';
toast.style.transform = 'translate(-50%)';
toast.style.left = '50%';
toast.style.top = '50%';
toast.style.padding = '10px';
toast.innerText = message;
toast.style.borderRadius = '7px';
toast.style.opacity = 1;
toast.style.fontSize = '12px';
toast.style.minHeight = '30px';
toast.style.minWidth = '70px';
toast.style.zIndex = 99999;
if(theme == 'dark'){
toast.style.backgroundColor = '#0f172a';
toast.style.border = '1px solid #f1f5f9';
toast.style.color = '#f8fafc';
} else {
toast.style.backgroundColor = '#f1f5f9';
toast.style.border = '1px solid #1e293b';
toast.style.color = '#0f172a';
}
node_ref.append(toast);
setTimeout(() => {
toast.remove()
}, 3000);
}
</script>
<div
on:mousedown={focus}
use:click_outside on:click_outside={loose_focus}
bind:this={node_ref} style="
opacity:0;
position: absolute;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
padding: 0px;
-webkit-font-smoothing: antialiased;"
program-id="{options.id}"
class="window absolute flex flex-col bg-xp-yellow {animation_enabled ? 'transition duration-300' : ''} {minimized ? `opacity-0` : ''}"
style:width="{options.width}px" style:height="{options.height}px"
style:min-width="{options.min_width}px" style:min-height="{options.min_height}px"
style:transform="{minimized ? `${translateX} ${translateY} scale(0.1)` : 'none'}"
style:background="{options.background}"
style:z-index="{z_index}" style:box-shadow="{z_index < $zIndex ? 'var(--window-box-shadow-inactive)' : 'var(--window-box-shadow)'}">
<div class="shrink-0">
<TitleBar bind:this={titlebar} options={options} inactive={z_index < $zIndex} maximized={maximized}
on_click_close={on_click_close} on_click_maximize={on_click_maximize} on_click_minimize={on_click_minimize}>
</TitleBar>
</div>
<div class="grow shrink-0 relative shadow-xl">
<slot name="content"></slot>
</div>
</div>
<svelte:options accessors={true}></svelte:options>

View File

@@ -0,0 +1,121 @@
import { queueProgram, clipboard, hardDrive } from '../../../store';
import { get } from 'svelte/store';
import { recycle_bin_id, protected_items } from '../../../system';
import * as fs from '../../../fs';
export let make = ({type, originator}) => {
//originator: program
return {
required_width: 180 + 20,
required_height: 27*6 + 20,
menu: [
[
{
name: 'Arrange Icons By',
items: [
{
name: 'Name'
},
{
name: 'Size'
},
{
name: 'Type'
},
{
name: 'Modified'
}
]
},
{
name: 'Refresh',
action: () => {
console.log('refresh');
let nodes = document.querySelectorAll('.fs-item');
for(let node of nodes){
node.classList.add('animate-blink');
}
setTimeout(() => {
for(let node of nodes){
node.classList.remove('animate-blink');
}
}, 1000);
}
}
],
[
{
name: 'Paste',
disabled: get(clipboard).length == 0,
action: () => {
fs.paste(originator.id);
}
},
{
name: 'Paste Shortcut',
disabled: true
}
],
[
{
name: 'New',
items: [
{
name: 'Folder',
icon: '/images/xp/icons/FolderClosed.png',
action: () => {
fs.new_fs_item('folder', '', 'New Folder', originator.id);
}
},
{
name: 'Shortcut',
icon: '/images/xp/icons/Shortcutoverlay.png'
},
{
name: 'Briefcase',
icon: '/images/xp/icons/Briefcase.png'
},
{
name: 'Bitmap Image',
icon: '/images/xp/icons/Bitmap.png',
action: () => {
fs.new_fs_item('file', '.bmp', 'New Bitmap Image', originator.id);
}
},
{
name: 'Text Document',
icon: '/images/xp/icons/TXT.png',
action: () => {
fs.new_fs_item('file', '.txt', 'New Text Document', originator.id);
}
},
{
name: 'Wave Sound',
icon: '/images/xp/icons/WMV.png',
action: () => {
fs.new_fs_item('file', '.wav', 'New Sound', originator.id);
}
},
{
name: 'Compressed (zipped) Folder',
icon: '/images/xp/icons/Zipfolder.png'
}
]
}
],
[
{
name: 'Properties',
action: () => {
queueProgram.set({
name: 'Display Properties',
icon: 'DisplayProperties.png',
path: './programs/display_properties.svelte'
})
}
}
]
]
}
}

View File

@@ -0,0 +1,267 @@
import { queueProgram, clipboard, selectingItems, hardDrive, clipboard_op, wallpaper } from '../../../store';
import { recycle_bin_id, protected_items, wallpapers_folder, supported_wallpaper_filetypes, doctypes, archive_exts } from '../../../system';
import * as utils from '../../../utils';
import { get } from 'svelte/store';
import * as fs from '../../../fs';
import short from 'short-uuid';
import FileSaver from 'file-saver';
export let make = ({type, originator}) => {
//originator: a wrapped fs item, i.e, file, folder, drive
// {item: item, open: fn(), my_computer_instance: obj})
return {
required_width: 180 + 20,
required_height: 27*11 + 20,
menu: [
[
...originator.item.parent != recycle_bin_id ? [
{
name: 'Open',
action: () => {originator.open(originator.item.id);},
font: 'bold',
},
{
name: 'Explore',
},
{
name: 'Search...',
disabled: originator.type == 'file'
}
] : [],
...originator.item.parent != recycle_bin_id
&& doctypes[originator.item.ext] != null
&& doctypes[originator.item.ext].length >= 2 ? [{
name: 'Open With',
items: doctypes[originator.item.ext].map(el => {
return {
name: el.name,
icon: el.icon,
action: () => queueProgram.set({
path: el.path,
fs_item: originator.item
})
}
})
}] : [],
...supported_wallpaper_filetypes.includes(originator.item.ext) ? [
{
name: 'Set as Desktop Wallpaper',
action: () => {
let new_id = short.generate();
fs.clone_fs(originator.item.id, wallpapers_folder, new_id);
wallpaper.set(new_id);
}
}
] : []
],
[
...archive_exts.includes(originator.item.ext) && originator.item.parent != recycle_bin_id ? [
{
name: 'Extract here...',
icon: '/images/xp/icons/RAR.png',
action: () => {
queueProgram.set({
path: './programs/winrar.svelte',
fs_item: originator.item
})
}
}
] : [],
...['file', 'folder'].includes(originator.item.type) && originator.item.parent != recycle_bin_id ? [
{
name: 'Add to archive...',
icon: '/images/xp/icons/RAR.png',
action: () => {
queueProgram.set({
path: './programs/zip.svelte',
fs_item: originator.item
})
}
}
] : []
],
[
...originator.item.parent != recycle_bin_id ? [
{
name: 'Send To',
items: [
...originator.item.type == 'file'
&& originator.item.storage_type != 'fake' ? [{
name: 'Local Computer (Download)',
icon: '/images/xp/icons/CopyingConflict.png',
action: async () => {
let file = await fs.get_file(originator.item.id);
let download = new File([file], originator.item.name, {
type: utils.ext_to_mime(originator.item.name)
});
FileSaver.saveAs(download);
}
}] : [],
{
name: 'Compressed (Zipped) Folder',
icon: '/images/xp/icons/Zipfolder.png',
action: () => {
queueProgram.set({
path: './programs/zip.svelte',
fs_item: originator.item
})
}
},
{
name: 'Desktop (create shortcut)',
icon: '/images/xp/icons/Desktop.png'
},
{
name: 'Mail Recipient',
icon: '/images/xp/icons/Email.png'
},
{
name: 'Floppy (A:)',
icon: '/images/xp/icons/FloppyDisk.png'
}
]
}
] : []
],
[
...protected_items.includes(originator.item.id) ? [] : [
{
name: 'Cut',
disabled: get(selectingItems).length == 0,
action: () => {
fs.cut();
}
}
],
...originator.item.type == 'drive' || originator.item.type == 'removable_storage' ? [] : [
{
name: 'Copy',
disabled: get(selectingItems).length == 0,
action: () => {
fs.copy();
}
}
],
... originator.item.type != 'file' && originator.item.parent != recycle_bin_id ? [{
name: 'Paste',
disabled: get(clipboard).length == 0,
action: () => {
fs.paste(originator.item.id);
}
}] : [],
],
[
...protected_items.includes(originator.item.id) ? [] : [
{
name: 'Delete',
action: () => {
let items = [...get(selectingItems)];
console.log(items)
let yes_action = () => {
if(originator.item.parent == recycle_bin_id){
for(let id of items){
fs.del_fs(id);
}
} else {
for(let id of items){
fs.clone_fs(id, recycle_bin_id, null);
fs.del_fs(id);
}
}
}
let filename = originator.item.name.length > 70 ? originator.item.name.slice(0,70) + '...' : originator.item.name;
let message = '';
let plural = '';
if(items.length == 1){
plural = '';
} else if(items.length == 2){
plural = ' and 1 other item';
} else if(items.length > 2){
plural = ` and ${items.length-1} other items`;
}
if(originator.item.parent == recycle_bin_id){
message = `Do you want to permanently delete ${filename}${plural}? This action can't be undone?`
} else {
message = `Do you want to move ${filename}${plural} to the Recycle Bin?`
}
let icon = originator.item.parent == recycle_bin_id ? '/images/xp/icons/DeleteConfirmation.png' : '/images/xp/icons/RecycleBinempty.png';
confirm_delete({
node_ref: originator.my_computer_instance?.window.node_ref || document.body,
title: 'Confirm Delete File',
icon,
message,
yes_action: yes_action,
no_action: () => {}
});
}
}
],
...protected_items.includes(originator.item.id) || originator.item.parent == recycle_bin_id ? [] : [
{
name: 'Rename',
action: () => {
selectingItems.set([originator.item.id]);
originator.rename();
}
}
]
],
[
{
name: 'Properties',
action: () => {
if(originator.item.type == 'drive' || originator.item.type == 'removable_storage'){
queueProgram.set({
path: './programs/disk_properties.svelte',
fs_item: originator.item
})
} else {
queueProgram.set({
path: './programs/properties.svelte',
fs_item: originator.item
})
}
}
}
]
]
}
}
async function confirm_delete({node_ref, title, message, icon, yes_action, no_action}){
const Dialog = (await import('../Dialog.svelte')).default;
let buttons = [
{
name: 'OK',
action: () => {
yes_action();
dialog.destroy();
},
focus: true
},
{
name: 'Cancel',
action: () => {
no_action();
dialog.destroy();
},
}
]
let dialog = new Dialog({
target: node_ref,
props:{
icon,
title,
message,
buttons,
}
})
dialog.self = dialog;
}

View File

@@ -0,0 +1,138 @@
import { queueProgram, clipboard, hardDrive } from '../../../store';
import { recycle_bin_id} from '../../../system';
import { get } from 'svelte/store';
import * as fs from '../../../fs';
export let make = ({type, originator}) => {
//originator: viewer
//originator: program
return {
required_width: 180 + 20,
required_height: 27*6 + 20,
menu: [
[
{
name: 'Arrange Icons By',
items: [
{
name: 'Name'
},
{
name: 'Size'
},
{
name: 'Type'
},
{
name: 'Modified'
}
]
},
{
name: 'Refresh',
action: () => {
console.log('refresh');
let nodes = document.querySelectorAll('.fs-item');
for(let node of nodes){
node.classList.add('animate-blink');
}
setTimeout(() => {
for(let node of nodes){
node.classList.remove('animate-blink');
}
}, 1000);
}
}
],
[
...originator.id != recycle_bin_id ? [
{
name: 'Paste',
disabled: get(clipboard).length == 0,
action: () => {
fs.paste(originator.id);
}
},
{
name: 'Paste Shortcut',
disabled: true
}
] : []
],
[
...originator.id != recycle_bin_id ? [
{
name: 'New',
items: [
{
name: 'Folder',
icon: '/images/xp/icons/FolderClosed.png',
action: () => {
fs.new_fs_item('folder', '', 'New Folder', originator.id);
}
},
{
name: 'Shortcut',
icon: '/images/xp/icons/Shortcutoverlay.png'
},
{
name: 'Briefcase',
icon: '/images/xp/icons/Briefcase.png'
},
{
name: 'Bitmap Image',
icon: '/images/xp/icons/Bitmap.png',
action: () => {
fs.new_fs_item('file', '.bmp', 'New Bitmap Image', originator.id);
}
},
{
name: 'Text Document',
icon: '/images/xp/icons/TXT.png',
action: () => {
fs.new_fs_item('file', '.txt', 'New Text Document', originator.id);
}
},
{
name: 'Wave Sound',
icon: '/images/xp/icons/WMV.png',
action: () => {
fs.new_fs_item('file', '.wav', 'New Sound', originator.id);
}
},
{
name: 'Compressed (zipped) Folder',
icon: '/images/xp/icons/Zipfolder.png'
}
]
}
] : []
],
[
{
name: 'Properties',
action: () => {
let fs_item = get(hardDrive)[originator.id];
if(fs_item.type == 'drive' || fs_item.type == 'removable_storage'){
queueProgram.set({
path: './programs/disk_properties.svelte',
fs_item
})
} else {
queueProgram.set({
path: './programs/properties.svelte',
fs_item
})
}
}
}
]
]
}
}

View File

@@ -0,0 +1,43 @@
export let make = ({type, originator}) => {
//originator: program
return {
required_width: 180 + 20,
required_height: 27*4 + 20,
menu: [
[
{
name: 'Minimize',
action: () => {originator.window.on_click_minimize();},
disabled: originator.window.minimized,
icon: '/images/xp/icons/tile_minimize.png',
icon_size: 10,
icon_type: 'monotone'
},
{
name: 'Restore',
action: () => {originator.window.restore();},
disabled: !originator.window.maximized && !originator.window.minimized,
icon: '/images/xp/icons/tile_restore.png',
icon_size: 10,
icon_type: 'monotone'
},
{
name: 'Maximize',
action: () => {originator.window.on_click_maximize();},
disabled: originator.window.maximized || !originator.window.options.resizable,
icon: '/images/xp/icons/tile_maximize.png',
icon_size: 10,
icon_type: 'monotone'
},
{
name: 'Close',
font: 'bold',
action: () => {originator.window.on_click_close()},
icon: '/images/xp/icons/tile_close.png',
icon_size: 10,
icon_type: 'monotone'
}
]
]
}
}

View File

@@ -0,0 +1,93 @@
import { queueProgram, clipboard, selectingItems, hardDrive, clipboard_op } from '../../../store';
import { recycle_bin_id, protected_items } from '../../../system';
import { get } from 'svelte/store';
import * as fs from '../../../fs';
export let make = ({type, originator}) => {
//originator: a wrapped fs item, i.e, file, folder, drive
// {item: item, open: fn(), my_computer_instance: obj}
return {
required_width: 180 + 20,
required_height: 27*3 + 20,
menu: [
[
{
name: 'Open',
action: () => {
let fs_item = get(hardDrive)[recycle_bin_id];
queueProgram.set({
path: './programs/my_computer.svelte',
fs_item: fs_item
})
},
font: 'bold',
}
],
[
{
name: 'Empty Recycle Bin',
action: () => {
let yes_action = () => {
let files = get(hardDrive)[recycle_bin_id].files;
let folders = get(hardDrive)[recycle_bin_id].folders;
for(let id of [...files, ...folders]){
fs.del_fs(id);
}
}
confirm_delete({
node_ref: document.body,
title: 'Confirm Delete File',
icon: '/images/xp/icons/DeleteConfirmation.png' ,
message: 'Do you want to permanently delete all files in the Recycle Bin? This action cannot be undone.',
yes_action: yes_action,
no_action: () => {}
});
}
},
{
name: 'Properties',
action: () => {
// queueProgram.set({
// path: './programs/properties.svelte',
// fs_item: originator.item
// })
}
}
]
]
}
}
async function confirm_delete({node_ref, title, message, icon, yes_action, no_action}){
const Dialog = (await import('../Dialog.svelte')).default;
let buttons = [
{
name: 'OK',
action: () => {
yes_action();
dialog.destroy();
},
focus: true
},
{
name: 'Cancel',
action: () => {
no_action();
dialog.destroy();
}
}
]
let dialog = new Dialog({
target: node_ref,
props:{
icon,
title,
message,
buttons
}
})
dialog.self = dialog;
}

View File

@@ -0,0 +1,71 @@
export function tooltip(element) {
let comp;
let timeout;
function mouseEnter(event) {
if(event.target !== element) return;
let tooltip_message = element.getAttribute('tooltip');
if(tooltip_message == null || tooltip_message == '') return;
comp = document.createElement('div');
let rect = element.getBoundingClientRect();
let estimated_width = 150;
let estimated_height = 30;
let screen_width = document.body.offsetWidth;
let screen_height = document.body.offsetHeight;
if(rect.y + rect.height > screen_height - estimated_height){
comp.style.bottom = `${rect.height+15}px`;
} else {
comp.style.top = `${rect.y + rect.height + 10}px`;
}
if(rect.x + rect.width > screen_width - estimated_width){
comp.style.right = `${20}px`;
} else {
comp.style.left = `${rect.x}px`;
}
comp.style.maxWidth = '150px';
comp.style.pointerEvents = 'none';
comp.style.position = 'absolute';
comp.style.zIndex = '999';
comp.style.backgroundColor = '#ece8cf';
comp.style.boxShadow = '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)';
comp.style.overflow = 'hidden';
comp.style.padding = '4px';
comp.style.fontFamily = 'MSSS';
comp.innerHTML = `
<p class=" line-clamp-1 leading-tight text-ellipsis" style="font-size:11px;">
${tooltip_message}
</p>`;
timeout = setTimeout(() => {
document.body.appendChild(comp);
setTimeout(() => {
comp.remove();
}, 3000);
}, 2000);
}
function mouseLeave(event) {
clearTimeout(timeout);
if(comp != null){
comp.remove();
}
}
element.addEventListener('mouseenter', mouseEnter);
element.addEventListener('mouseleave', mouseLeave);
return {
destroy() {
element.removeEventListener('mouseenter', mouseEnter);
element.removeEventListener('mouseleave', mouseLeave);
}
}
}

147
src/lib/dir_parser.js Normal file
View File

@@ -0,0 +1,147 @@
/**
* Traversing directory using promises
**/
const traverseDirectory = (entry) => {
const reader = entry.createReader();
return new Promise((resolveDirectory) => {
const iterationAttempts = [];
const errorHandler = () => {};
function readEntries() {
reader.readEntries((batchEntries) => {
if (!batchEntries.length) {
resolveDirectory(Promise.all(iterationAttempts))
} else {
iterationAttempts.push(Promise.all(batchEntries.map((batchEntry) => {
if (batchEntry.isDirectory) {
return traverseDirectory(batchEntry);
}
return Promise.resolve(batchEntry);
})));
readEntries();
}
}, errorHandler);
}
readEntries();
});
}
const packageFile = (file, entry) => {
let object = {
fileObject: file,
fullPath: entry ? entry.fullPath : '',
lastModified: file.lastModified,
lastModifiedDate: file.lastModifiedDate,
name: file.name,
size: file.size,
type: file.type,
webkitRelativePath: file.webkitRelativePath
}
return object;
}
const getFile = (entry) => {
return new Promise((resolve) => {
entry.file((file) => {
resolve(packageFile(file, entry));
})
})
}
const handleFilePromises = (promises, fileList) => {
return Promise.all(promises).then((files) => {
files.forEach((file) => {
fileList.push(file);
});
return fileList;
})
}
const getDataTransferFiles = (dataTransfer) => {
const dataTransferFiles = [];
const folderPromises = [];
const filePromises = [];
[].slice.call(dataTransfer.items).forEach((listItem) => {
let supported_method;
if(typeof listItem['webkitGetAsEntry'] === 'function'){
supported_method = 'webkitGetAsEntry';
} else {
supported_method = 'getAsEntry'
}
const entry = listItem[supported_method]();
if (entry) {
if (entry.isDirectory) {
folderPromises.push(traverseDirectory(entry));
} else {
filePromises.push(getFile(entry));
}
} else {
dataTransferFiles.push(listItem);
}
});
if (folderPromises.length) {
const flatten = (array) => array.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []);
return Promise.all(folderPromises).then((fileEntries) => {
const flattenedEntries = flatten(fileEntries);
flattenedEntries.forEach((fileEntry) => {
filePromises.push(getFile(fileEntry));
});
return handleFilePromises(filePromises, dataTransferFiles);
});
} else if (filePromises.length) {
return handleFilePromises(filePromises, dataTransferFiles);
}
return Promise.resolve(dataTransferFiles);
}
// Use this function by passing the drop or change event.
const getDroppedOrSelectedFiles = (event) => {
const dataTransfer = event.dataTransfer;
if (dataTransfer && dataTransfer.items) {
return getDataTransferFiles(dataTransfer)
.then((fileList) => {
return Promise.resolve(fileList);
})
}
const files = [];
const dragDropFileList = dataTransfer && dataTransfer.files;
const inputFieldFileList = event.target && event.target.files;
const fileList = dragDropFileList || inputFieldFileList || [];
for (let i = 0; i < fileList.length; i++) {
files.push(packageFile(fileList[i]));
}
return Promise.resolve(files);
}
export async function parse_dir(e){
let files = await getDroppedOrSelectedFiles(e);
let result = {};
for(let file of files){
let comps = file.fullPath.split('/').filter(el => el != '');
comps.reduce((r, el, index) => {
if(index != comps.length - 1){
if(r[el] == null){
r[el] = {}
}
} else {
r[el] = file.fileObject;
}
return r[el];
}, result);
}
return result;
}

56
src/lib/docx/docx2html.js Normal file
View File

@@ -0,0 +1,56 @@
const monospaceFonts = ["consolas", "courier", "courier new"];
let options = {
transformDocument: mammoth.transforms.paragraph(transformParagraph),
preserveColors: true,
preserveFonts: true,
styleMap: [
"p[style-name='Title'] => h1:fresh",
"p[style-name='Heading 1'] => h1:fresh",
"p[style-name='Heading 2'] => h2:fresh",
"p[style-name='Heading 3'] => h3:fresh",
"p[style-name='Heading 4'] => h4:fresh",
"p[style-name='Heading 5'] => h5:fresh",
"p[style-name='Heading 6'] => h6:fresh",
],
convertImage: mammoth.images.imgElement(function(image) {
return image.read().then(function(buffer) {
let file = new File([buffer], {type: image.contentType})
let url = URL.createObjectURL(file);
return {
src: url,
width: image.width,
height: image.height,
style: image.style
};
});
})
}
export async function docx2html(arrayBuffer){
try {
let result = await mammoth.convertToHtml({arrayBuffer}, options);
return result.value;
} catch (error) {
console.log(error);
return '';
}
}
function transformParagraph(paragraph) {
var runs = mammoth.transforms.getDescendantsOfType(paragraph, "run");
var isMatch = runs.length > 0 && runs.every(function(run) {
return run.font && monospaceFonts.indexOf(run.font.toLowerCase()) !== -1;
});
if (isMatch) {
return {
...paragraph,
styleId: "code",
styleName: "Code"
};
} else {
return paragraph;
}
}

666
src/lib/docx/html2docx.js Normal file
View File

@@ -0,0 +1,666 @@
import { Document, Packer, Paragraph, TextRun, ImageRun, AlignmentType, ExternalHyperlink,
Table, TableRow, TableCell, WidthType, HeadingLevel, ConcreteNumbering, Numbering,
HorizontalPositionRelativeFrom, VerticalPositionRelativeFrom, HorizontalPositionAlign, VerticalPositionAlign,
TextWrappingType, TextWrappingSide } from 'docx';
import * as fs from '../fs';
const COLORS = {
"black": "000000",
"silver": "c0c0c0",
"gray": "808080",
"white": "ffffff",
"maroon": "800000",
"red": "ff0000",
"purple": "800080",
"fuchsia": "ff00ff",
"green": "008000",
"lime": "00ff00",
"olive": "808000",
"yellow": "ffff00",
"navy": "000080",
"blue": "0000ff",
"teal": "008080",
"aqua": "00ffff",
"aliceblue": "f0f8ff",
"antiquewhite": "faebd7",
"aquamarine": "7fffd4",
"azure": "f0ffff",
"beige": "f5f5dc",
"bisque": "ffe4c4",
"blanchedalmond": "ffebcd",
"blueviolet": "8a2be2",
"brown": "a52a2a",
"burlywood": "deb887",
"cadetblue": "5f9ea0",
"chartreuse": "7fff00",
"chocolate": "d2691e",
"coral": "ff7f50",
"cornflowerblue": "6495ed",
"cornsilk": "fff8dc",
"crimson": "dc143c",
"cyan": "00ffff",
"darkblue": "00008b",
"darkcyan": "008b8b",
"darkgoldenrod": "b8860b",
"darkgray": "a9a9a9",
"darkgreen": "006400",
"darkgrey": "a9a9a9",
"darkkhaki": "bdb76b",
"darkmagenta": "8b008b",
"darkolivegreen": "556b2f",
"darkorange": "ff8c00",
"darkorchid": "9932cc",
"darkred": "8b0000",
"darksalmon": "e9967a",
"darkseagreen": "8fbc8f",
"darkslateblue": "483d8b",
"darkslategray": "2f4f4f",
"darkslategrey": "2f4f4f",
"darkturquoise": "00ced1",
"darkviolet": "9400d3",
"deeppink": "ff1493",
"deepskyblue": "00bfff",
"dimgray": "696969",
"dimgrey": "696969",
"dodgerblue": "1e90ff",
"firebrick": "b22222",
"floralwhite": "fffaf0",
"forestgreen": "228b22",
"gainsboro": "dcdcdc",
"ghostwhite": "f8f8ff",
"gold": "ffd700",
"goldenrod": "daa520",
"greenyellow": "adff2f",
"grey": "808080",
"honeydew": "f0fff0",
"hotpink": "ff69b4",
"indianred": "cd5c5c",
"indigo": "4b0082",
"ivory": "fffff0",
"khaki": "f0e68c",
"lavender": "e6e6fa",
"lavenderblush": "fff0f5",
"lawngreen": "7cfc00",
"lemonchiffon": "fffacd",
"lightblue": "add8e6",
"lightcoral": "f08080",
"lightcyan": "e0ffff",
"lightgoldenrodyellow": "fafad2",
"lightgray": "d3d3d3",
"lightgreen": "90ee90",
"lightgrey": "d3d3d3",
"lightpink": "ffb6c1",
"lightsalmon": "ffa07a",
"lightseagreen": "20b2aa",
"lightskyblue": "87cefa",
"lightslategray": "778899",
"lightslategrey": "778899",
"lightsteelblue": "b0c4de",
"lightyellow": "ffffe0",
"limegreen": "32cd32",
"linen": "faf0e6",
"magenta": "ff00ff",
"mediumaquamarine": "66cdaa",
"mediumblue": "0000cd",
"mediumorchid": "ba55d3",
"mediumpurple": "9370db",
"mediumseagreen": "3cb371",
"mediumslateblue": "7b68ee",
"mediumspringgreen": "00fa9a",
"mediumturquoise": "48d1cc",
"mediumvioletred": "c71585",
"midnightblue": "191970",
"mintcream": "f5fffa",
"mistyrose": "ffe4e1",
"moccasin": "ffe4b5",
"navajowhite": "ffdead",
"oldlace": "fdf5e6",
"olivedrab": "6b8e23",
"orange": "ffa500",
"orangered": "ff4500",
"orchid": "da70d6",
"palegoldenrod": "eee8aa",
"palegreen": "98fb98",
"paleturquoise": "afeeee",
"palevioletred": "db7093",
"papayawhip": "ffefd5",
"peachpuff": "ffdab9",
"peru": "cd853f",
"pink": "ffc0cb",
"plum": "dda0dd",
"powderblue": "b0e0e6",
"rosybrown": "bc8f8f",
"royalblue": "4169e1",
"saddlebrown": "8b4513",
"salmon": "fa8072",
"sandybrown": "f4a460",
"seagreen": "2e8b57",
"seashell": "fff5ee",
"sienna": "a0522d",
"skyblue": "87ceeb",
"slateblue": "6a5acd",
"slategray": "708090",
"slategrey": "708090",
"snow": "fffafa",
"springgreen": "00ff7f",
"steelblue": "4682b4",
"tan": "d2b48c",
"thistle": "d8bfd8",
"tomato": "ff6347",
"turquoise": "40e0d0",
"violet": "ee82ee",
"wheat": "f5deb3",
"whitesmoke": "f5f5f5",
"yellowgreen": "9acd32"
}
export async function html2docx(html){
let document = (new DOMParser()).parseFromString(html,'text/html');
let docx_elements = [];
let nodes = Array.from(document.querySelectorAll('p,pre,table,h1,h2,h3,h4,h5,h6,ul,ol'));
nodes = nodes.filter(node => {
return !nodes
.filter(el => el != node)
.some(el => el.contains(node));
});
for(let node of nodes){
let instance = nodes.indexOf(node);
if(node.nodeName == 'P' || node.nodeName == 'PRE'){
docx_elements.push(await build_paragraph(node));
} else if(node.nodeName == 'TABLE'){
docx_elements.push(await build_table(node));
} else if(['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(node.nodeName)){
docx_elements.push(await build_heading(node));
} else if(node.nodeName == 'UL'){
docx_elements.push(...await build_ul(node, instance));
} else if(node.nodeName == 'OL'){
docx_elements.push(...await build_ol(node, instance));
}
}
let docx = new Document({
sections: [{
children: docx_elements
}],
numbering:{
config:[{
reference: 'arabic',
levels: [
{
level: 0,
format: "decimal",
text: "%1",
alignment: AlignmentType.START,
style: {
paragraph: {
indent: { left: 300, hanging: 200 },
},
},
},
{
level: 1,
format: "decimal",
text: "%1.%2",
alignment: AlignmentType.START,
style: {
paragraph: {
indent: { left: 600, hanging: 200 },
},
},
},
],
}]
},
})
let blob = await Packer.toBlob(docx);
return blob;
}
async function build_paragraph(node){
let style = parse_style(node);
if(style.size == null) style.size = 24;
if(style.font == null && node.nodeName == 'PRE') style.font = 'Courier New';
let children = await build_child_nodes(node, style);
let alignment = get_align(node);
let border = parse_border(node);
if(node.parentElement.nodeName == 'BLOCKQUOTE'){
if(border.left == null){
border.left = {color: 'cbd5e1', size: 16, space: 1, style: 'single'}
}
if(style.indent == null || style.indent.left == 0){
style.indent = {left: 80}
}
}
let paragraph = new Paragraph({
alignment,
indent: style.indent,
children,
border
});
return paragraph;
}
async function build_table(node){
let rows = [];
for(let row of node.querySelectorAll('tr')){
let cells = [];
for(let cell of row.querySelectorAll('th, td')){
cells.push(new TableCell({
children: [new Paragraph({children: await build_child_nodes(cell)})]
}));
}
rows.push(new TableRow({
children: cells
}))
}
let number_of_columns = node.querySelector('tr').querySelectorAll('th, td').length;
let table = new Table({
rows,
width: 0,
columnWidths: Array(number_of_columns).fill(Math.floor(9638/number_of_columns), 0, number_of_columns)
});
return table;
}
async function build_heading(node){
let style = parse_style(node);
let children = await build_child_nodes(node, style);
let alignment = get_align(node);
let heading;
switch (node.nodeName) {
case 'H1':
heading = HeadingLevel.HEADING_1
break;
case 'H2':
heading = HeadingLevel.HEADING_2;
break;
case 'H3':
heading = HeadingLevel.HEADING_3;
break;
case 'H4':
heading = HeadingLevel.HEADING_4;
break;
case 'H5':
heading = HeadingLevel.HEADING_5;
break;
case 'H6':
heading = HeadingLevel.HEADING_6;
break;
default:
break;
}
let border = parse_border(node);
let paragraph = new Paragraph({
alignment,
children,
indent: style.indent,
heading,
border
});
return paragraph;
}
async function build_ul(node, instance){
let list = [];
for(let li of node.querySelectorAll(':scope > li')){
list.push(new Paragraph({
children: await build_child_nodes(li),
bullet: {level: 0, instance}
}))
if(li.querySelectorAll == null) continue;
for(let sub_li of li.querySelectorAll('ul li')){
list.push(new Paragraph({
children: await build_child_nodes(sub_li),
bullet: {level: 1, instance}
}))
}
}
return list;
}
async function build_ol(node, instance){
let list = [];
for(let li of node.querySelectorAll(':scope > li')){
list.push(new Paragraph({
children: await build_child_nodes(li),
numbering: {reference: 'arabic', instance, level: 0}
}))
if(li.querySelectorAll == null) continue;
for(let sub_li of li.querySelectorAll('ol li')){
list.push(new Paragraph({
children: await build_child_nodes(sub_li),
numbering: {reference: 'arabic', instance, level: 1}
}))
}
}
return list;
}
async function build_child_nodes(node, inherit_attr){
if(inherit_attr == null) inherit_attr = {};
let values = [];
let children = node.childNodes;
for(let child of children){
if(child.nodeName == '#text'){
let text_run = new TextRun({
text: child.nodeValue,
bold: inherit_attr.bold,
italics: inherit_attr.italics,
subScript: inherit_attr.subScript,
superScript: inherit_attr.superScript,
strike: inherit_attr.strike,
underline: inherit_attr.underline ? {} : null,
color: inherit_attr.color,
shading: inherit_attr.shading,
size: inherit_attr.size,
allCaps: inherit_attr.allCaps,
font: inherit_attr.font,
style: inherit_attr.style
})
values = [...values, text_run];
} else if(child.nodeName == 'A' && child.getAttribute('href')){
let link = new ExternalHyperlink({
children: await build_child_nodes(child, {style: 'Hyperlink'}),
link: child.getAttribute('href')
})
values = [...values, link];
} else if(child.nodeName == 'IMG') {
let buffer = await fs.buffer_from_url(child.src);
let floating = parse_image_floating(child);
let image_run = new ImageRun({
data: buffer,
transformation: {width: child.width, height: child.height},
floating
})
values = [...values, image_run];
} else if(node.childNodes.length > 0 && !['UL', 'OL'].includes(node.nodeName)){
let passed_down_style = {...inherit_attr, ...parse_style(child)};
if(child.nodeName == 'STRONG') passed_down_style.bold = true;
if(child.nodeName == 'EM') passed_down_style.italics = true;
if(child.nodeName == 'SUB') passed_down_style.subScript = true;
if(child.nodeName == 'SUP') passed_down_style.superScript = true;
if(child.nodeName == 'S') passed_down_style.strike = true;
if(child.nodeName == 'U') passed_down_style.underline = true;
if(child.nodeName == 'A') {
passed_down_style.anchor = child.getAttribute('href');
passed_down_style.underline = true;
}
values = [...values, ...await build_child_nodes(child, passed_down_style)];
}
}
return values;
}
function parse_style(node){
let style = {};
let raw_style = (node.getAttribute('style') || '').split(';');
for(let el of raw_style){
let values = el.trim().split(':');
if(values.length == 2){
style[values[0].trim()] = values[1].trim();
}
}
let fill = to_hex(style['background-color']);
if(fill){
style['shading'] = {fill};
}
style['color'] = to_hex(style['color']);
if(style['font-family']){
style['font'] = style['font-family'].split(',')[0];
if(style['font']){
style['font'] = style['font'].split('\'').join('');
}
}
style['size'] = to_halfpoint(style['font-size']);
let indent_left = to_halfpoint(style['padding-left']);
if(!isNaN(indent_left)){
//indent_left is in halfpoint,
//indentations in OpenXML are measured in 1/20 of a point
style['indent'] = {left: indent_left*10}
}
if(style['text-transform'] == 'uppercase'){
style['allCaps'] = true;
}
if(style['text-transform'] == 'capitalize'){
style['smallCaps'] = true;
}
if(style['text-decoration'] == 'line-through'){
style['strike'] = true;
}
if(style['text-decoration'] == 'underline'){
style['underline'] = true;
}
if(style['font-style'] == 'italic'){
style['italics'] = true;
}
if(style['font-weight'] == 'bold' || parseInt(style['font-weight']) >= 700){
style['bold'] = true;
}
let allow_attrs = ['color', 'shading', 'size', 'indent', 'allCaps', 'allCaps', 'strike', 'font', 'italics','underline', 'bold'];
for(let key of Object.keys(style)){
if(!allow_attrs.includes(key) || style[key] == null){
delete style[key];
}
}
return style;
}
function to_halfpoint(str){
if(str == null || str == '') return null;
str = str.trim();
let unit;
if(str.endsWith('pt')){
unit = 'pt'
} else if(str.endsWith('px')){
unit = 'px';
}
if(unit){
let value = parseInt(str.split(unit).join(''));
if(isNaN(value)) return null;
if(unit == 'px'){
value = 2*Math.ceil((72*value)/96)
} else if(unit == 'pt'){
value = 2*value;
}
return value;
} else {
return null;
}
}
function to_hex(str){
if(str == null || str == '') return null;
str = str.trim();
if(COLORS[str] != null) return COLORS[str];
let color;
if(str.includes('rgb')){
color = rgb_to_hex(str);
} else {
color = str.split('#').join('').trim();
}
if(color.length == 3){
color = color + color;
}
if(color == null || !/^[0-9A-F]{6}$/i.test(color)){
color == null
}
return color;
}
function rgb_to_hex(rgb) {
// Choose correct separator
let sep = rgb.indexOf(",") > -1 ? "," : " ";
// Turn "rgb(r,g,b)" into [r,g,b]
rgb = rgb.substr(4).split(")")[0].split(sep);
let r = (+rgb[0]).toString(16),
g = (+rgb[1]).toString(16),
b = (+rgb[2]).toString(16);
if (r.length == 1)
r = "0" + r;
if (g.length == 1)
g = "0" + g;
if (b.length == 1)
b = "0" + b;
return r + g + b;
}
function parse_border(node){
let style = {};
let raw_style = (node.getAttribute('style') || '').split(';');
for(let el of raw_style){
let values = el.trim().split(':');
if(values.length == 2){
style[values[0].trim()] = values[1].trim();
}
}
let top, right, bottom, left;
if(style['border'] != null){
let [size, border_style, ...color] = style['border'].split(' ');
color = to_hex(color.join('').trim());
size = to_halfpoint(size)*4;
top = {color,size, space:1,style: 'single'}
right = {color,size, space:1,style: 'single'}
bottom = {color,size, space:1,style: 'single'}
left = {color,size, space:1,style: 'single'}
}
if(style['border-left'] != null){
let [size, border_style, ...color] = style['border-left'].split(' ');
color = to_hex(color.join('').trim());
size = to_halfpoint(size)*4;
left = {color,size, space:1,style: 'single'}
}
if(style['border-right'] != null){
let [size, border_style, ...color] = style['border-right'].split(' ');
color = to_hex(color.join('').trim());
size = to_halfpoint(size)*4;
right = {color,size, space:1,style: 'single'}
}
if(style['border-top'] != null){
let [size, border_style, ...color] = style['border-top'].split(' ');
color = to_hex(color.join('').trim());
size = to_halfpoint(size)*4;
top = {color,size, space:1,style: 'single'}
}
if(style['border-bottom'] != null){
let [size, border_style, ...color] = style['border-bottom'].split(' ');
color = to_hex(color.join('').trim());
size = to_halfpoint(size)*4;
bottom = {color,size, space:1,style: 'single'}
}
return {top, right, bottom, left};
}
function parse_image_floating(node){
let style = {};
let raw_style = (node.getAttribute('style') || '').split(';');
for(let el of raw_style){
let values = el.trim().split(':');
if(values.length == 2){
style[values[0].trim()] = values[1].trim();
}
}
let verticalPosition = {
relative: VerticalPositionRelativeFrom.TOP_MARGIN,
align: VerticalPositionAlign.TOP
}
let margin = {
top: 360000,
right: 360000,
bottom: 360000,
left: 360000
}
if(style['margin-left'] == 'auto' && style['margin-right'] == 'auto'){
return {
horizontalPosition: {
relative: HorizontalPositionRelativeFrom.COLUMN,
align: HorizontalPositionAlign.CENTER,
},
verticalPosition, margin,
wrap: {type: TextWrappingType.TOP_AND_BOTTOM, side: TextWrappingSide.BOTH_SIDES}
}
}
if(style['float'] == 'left'){
return {
horizontalPosition: {
relative: HorizontalPositionRelativeFrom.COLUMN,
align: HorizontalPositionAlign.LEFT,
},
verticalPosition, margin,
wrap: {type:TextWrappingType.SQUARE, side: TextWrappingSide.RIGHT}
}
}
if(style['float'] == 'right'){
return {
horizontalPosition: {
relative: HorizontalPositionRelativeFrom.COLUMN,
align: HorizontalPositionAlign.RIGHT,
},
verticalPosition, margin,
wrap: {type:TextWrappingType.SQUARE, side: TextWrappingSide.LEFT}
}
}
return null;
}
function get_align(node){
let raw_style = node.getAttribute('style');
if(raw_style == null) return AlignmentType.LEFT;
let style = {};
for(let pair of raw_style.split(';')){
if(pair.split(':').length != 2) continue;
let key = pair.split(':')[0].trim();
let value = pair.split(':')[1].trim();
style[key] = value;
}
switch (style['text-align']) {
case 'left':
return AlignmentType.LEFT;
case 'center':
return AlignmentType.CENTER;
case 'justify':
return AlignmentType.JUSTIFIED;
case 'right':
return AlignmentType.RIGHT;
default:
return AlignmentType.LEFT;
}
}

2
src/lib/docx/index.js Normal file
View File

@@ -0,0 +1,2 @@
export { docx2html } from './docx2html';
export { html2docx } from './html2docx';

88
src/lib/finder.js Normal file
View File

@@ -0,0 +1,88 @@
import { hardDrive } from './store';
import { my_computer} from './system';
import { get } from 'svelte/store';
let computer = my_computer.map(el => get(hardDrive)[el]);
let drives = computer.filter(item => item.type == 'drive' || item.type == 'removable_storage');
export function to_url(id){
if(id == null || get(hardDrive)[id] == null) return null;
let url = '';
let current_location = get(hardDrive)[id];
url = current_location.name + '\\' + url;
if(current_location.parent == null) return url;
do {
current_location = get(hardDrive)[current_location.parent];
url = current_location.name + '\\' + url;
console.log(current_location);
} while (current_location.parent != null && current_location.parent.length != 0)
if(url[url.length - 1] == '\\'){
url = url.slice(0, url.length-1);
}
return url;
}
export function to_id_nocase(url){
if(url == null || url.trim().length == 0) return null;
let path_components = url.split('\\').filter(item => item.trim().length > 0).map(item => item.trim());
if(path_components.length == 0) return null;
let drive = drives.find(item => item.name.toLowerCase() == path_components[0].toLowerCase());
if(drive == null) return null;
if(path_components.length == 1) return drive.id;
drive = get(hardDrive)[drive.id];
let current_location = drive;
for(let i = 1; i < path_components.length; i++){
console.log(i);
console.log(path_components[i]);
current_location = [
...current_location.files.map(id => get(hardDrive)[id]),
...current_location.folders.map(id => get(hardDrive)[id])
]
.find(item => item?.name?.toLowerCase() == path_components[i].toLowerCase());
console.log(current_location);
if(current_location == null) return null;
if(i == path_components.length - 1) return current_location.id;
}
}
export function to_id(url){
if(url == null || url.trim().length == 0) return null;
let path_components = url.split('\\').filter(item => item.trim().length > 0).map(item => item.trim());
console.log(path_components);
if(path_components.length == 0) return null;
let drive = drives.find(item => item.name == path_components[0]);
if(drive == null) return null;
if(path_components.length == 1) return drive.id;
drive = get(hardDrive)[drive.id];
let current_location = drive;
for(let i = 1; i < path_components.length; i++){
console.log(i);
console.log(path_components[i]);
current_location = [
...current_location.files.map(id => get(hardDrive)[id]),
...current_location.folders.map(id => get(hardDrive)[id])
]
.find(item => item?.name == path_components[i]);
console.log(current_location);
if(current_location == null) return null;
if(i == path_components.length - 1) return current_location.id;
}
}

399
src/lib/fs.js Normal file
View File

@@ -0,0 +1,399 @@
import { queueProgram, clipboard, selectingItems, hardDrive, clipboard_op } from './store';
import { recycle_bin_id, protected_items } from './system';
import * as utils from './utils';
import { get } from 'svelte/store';
import short from 'short-uuid';
import * as util from './utils';
import * as idb from 'idb-keyval';
import * as finder from './finder';
import {Buffer} from 'buffer';
export function copy(){
clipboard_op.set('copy');
clipboard.set(get(selectingItems));
console.log('copy');
}
export function cut(){
clipboard_op.set('cut');
clipboard.set(get(selectingItems));
console.log('cut');
}
export function paste(id, new_id=null){
console.log('paste to', id);
console.log('clipboard_op', get(clipboard_op));
console.log(get(hardDrive)[id]);
if(get(hardDrive)[id] == null || get(hardDrive)[id].type == 'file'){
console.log('target is not a dir');
return;
}
if(get(clipboard).length == 0){
console.log('clipboard is empty');
return;
}
for(let fs_id of get(clipboard)){
clone_fs(fs_id, id, new_id);
if(get(clipboard_op) == 'cut'){
del_fs(fs_id);
}
}
clipboard_op.set('copy')
clipboard.set([]);
}
export function del_fs(id){
if(protected_items.includes(id)){
console.log(id, 'is protected');
return;
}
let obj = get(hardDrive)[id];
let child_ids = [
...obj.files,
...obj.folders
]
if(get(hardDrive)[obj.parent] != null){
console.log('delete from parent', obj.parent)
hardDrive.update(data => {
data[obj.parent].files = data[obj.parent].files.filter(el => el != obj.id);
data[obj.parent].folders = data[obj.parent].folders.filter(el => el != obj.id);
return data;
})
}
hardDrive.update(data => {
delete data[id];
return data;
})
for(let child_id of child_ids){
del_fs(child_id);
}
}
export function clone_fs(obj_current_id, parent_id, new_id=null){
let obj = {...get(hardDrive)[obj_current_id]};
if(new_id == null){
obj.id = short.generate();
} else {
obj.id = new_id;
}
obj.parent = parent_id;
let parent_items_names = [
...get(hardDrive)[parent_id].files.map(el => get(hardDrive)[el].name),
...get(hardDrive)[parent_id].folders.map(el => get(hardDrive)[el].name),
]
let appendix = 2;
let basename = obj.basename;
while(parent_items_names.includes(basename + obj.ext)){
basename = obj.basename + ' ' + appendix;
appendix++;
}
obj.basename = basename;
obj.name = basename + obj.ext;
//backup files & folders
console.log(obj)
let files = [...obj.files];
let folders = [...obj.folders];
obj.files = [];
obj.folders = [];
//save to hard drive
hardDrive.update(data => {
data[obj.id] = obj;
return data;
})
console.log('cloning', obj.id)
if(obj.type == 'file'){
hardDrive.update(data => {
data[parent_id].files.push(obj.id);
return data;
})
} else if(obj.type == 'folder'){
hardDrive.update(data => {
data[parent_id].folders.push(obj.id);
return data;
})
}
//recursively clone child items
for(let child of [...files, ...folders]){
clone_fs(child, obj.id);
}
}
export async function new_fs_item(type, ext, seedname, parent_id, file=null){
if(type == null || seedname == null || parent_id == null){
return;
}
let item = {
"id": short.generate(),
"type": type,
"path": "",
"name": "",
"storage_type": "local",
"url": short.generate(),
"ext": ext,
"level": 0,
"parent": parent_id,
"size": 1,
"files": [],
"folders": [],
"basename": ""
}
let files = get(hardDrive)[parent_id].files.map(el => get(hardDrive)[el]);
let folders = get(hardDrive)[parent_id].folders.map(el => get(hardDrive)[el]);
let parent_items_names = [
...files.map(el => el.name),
...folders.map(el => el.name)
]
let appendix = 2;
seedname = utils.sanitize_filename(seedname);
let basename = seedname;
while(parent_items_names.includes(basename + ext)){
basename = seedname + ' ' + appendix;
appendix++;
}
item.basename = basename;
item.name = basename + item.ext;
if(file != null){
await idb.set(item.url, file);
item.size = Math.ceil(file.size/1024);
} else if(type == 'file'){
console.log('fetch empty file')
file = await file_from_url(`/empty/empty${item.ext}`, item.name);
await idb.set(item.url, file);
item.size = Math.ceil(file.size/1024);
} else {
item.url = '';
}
hardDrive.update(data => {
data[item.id] = item;
return data;
})
if(type == 'file'){
hardDrive.update(data => {
data[parent_id].files.push(item.id);
return data;
})
} else if (type == 'folder'){
hardDrive.update(data => {
data[parent_id].folders.push(item.id);
return data;
})
}
return item.id;
}
export async function new_fs_item_raw(item, parent_id){
if(parent_id == null){
return;
}
item.id = short.generate();
item.parent = parent_id;
if(!['file', 'folder'].includes(item.type)){
item.type = 'file';
}
if(item.storage_type == null){
item.storage_type = 'local'
}
if(item.ext == null){
item.ext = '';
}
if(item.icon == null){
item.icon = '/images/xp/icons/ApplicationWindow.png'
}
if(item.files == null){
item.files = [];
}
if(item.folders == null){
item.folders = [];
}
let files = get(hardDrive)[parent_id].files.map(el => get(hardDrive)[el]);
let folders = get(hardDrive)[parent_id].folders.map(el => get(hardDrive)[el]);
let parent_items_names = [
...files.map(el => el.name),
...folders.map(el => el.name)
]
let appendix = 2;
let seedname = utils.sanitize_filename(item.basename);
let basename = seedname;
while(parent_items_names.includes(basename + item.ext)){
basename = seedname + ' ' + appendix;
appendix++;
}
item.basename = basename;
item.name = basename + item.ext;
if(item.file != null){
item.url = short.generate();
await idb.set(item.url, item.file);
item.size = Math.ceil(file.size/1024);
delete item.file;
} else if(item.executable){
item.url = './programs/webapp.svelte';
}
hardDrive.update(data => {
data[item.id] = item;
return data;
})
if(item.type == 'file'){
hardDrive.update(data => {
data[parent_id].files.push(item.id);
return data;
})
} else if (item.type == 'folder'){
hardDrive.update(data => {
data[parent_id].folders.push(item.id);
return data;
})
}
return item.id;
}
export function get_path(id){
return finder.to_url(id);
}
export async function save_file(fs_id, file){
if(get(hardDrive)[fs_id] == null){
console.log(fs_id, 'not exist');
return;
}
let url = short.generate();
await idb.set(url, file);
hardDrive.update(data => {
data[fs_id].url = url;
data[fs_id].storage_type = 'local';
return data;
})
}
export async function save_file_as(basename, ext, file, parent_id, new_id=null){
ext = ext.toLowerCase();
if(util.extname(basename) == ext){
basename = util.basename(basename, ext);
}
let url = short.generate();
await idb.set(url, file);
if(new_id == null){
new_id = short.generate();
}
let obj = {
"id": new_id,
"type": 'file',
"path": "",
"name": basename + ext,
"storage_type": "local",
"url": url,
"ext": ext,
"level": 0,
"parent": parent_id,
"size": Math.round(file.size/1024),
"files": [],
"folders": [],
"basename": basename
}
let parent_items_names = [
...get(hardDrive)[parent_id].files.map(el => get(hardDrive)[el].name),
...get(hardDrive)[parent_id].folders.map(el => get(hardDrive)[el].name),
]
let appendix = 2;
basename = obj.basename;
while(parent_items_names.includes(basename + obj.ext)){
basename = obj.basename + ' ' + appendix;
appendix++;
}
obj.basename = basename;
obj.name = basename + obj.ext;
hardDrive.update(data => {
data[obj.id] = obj;
data[parent_id].files.push(obj.id);
return data;
})
}
export async function get_file(id){
let fs_item = get(hardDrive)[id];
let file;
if(fs_item.storage_type == 'remote'){
file = await file_from_url(fs_item.url);
} else if(fs_item.storage_type == 'local') {
file = await idb.get(fs_item.url);
console.log(file);
}
file = new File([file], fs_item.name, {type: file.type})
return file;
}
export async function get_url(id){
let fs_item = get(hardDrive)[id];
if(fs_item.storage_type == 'remote'){
return fs_item.url;
} else if(fs_item.storage_type == 'local') {
let file = await idb.get(fs_item.url);
return URL.createObjectURL(file);
}
}
export async function file_from_url(url, name, defaultType = 'image/jpeg'){
try {
const response = await fetch(url);
const data = await response.blob();
return new File([data], name, {
type: data.type || defaultType,
});
} catch (error) {
return new File([''], 'empty.txt', {
type: 'text/plain'
})
}
}
export async function array_buffer_from_url(url){
let file = await file_from_url(url);
return await file.arrayBuffer();
}
export async function buffer_from_url(url){
let array_buffer = await array_buffer_from_url(url);
console.log(array_buffer)
return Buffer.from(array_buffer);
}

View File

@@ -0,0 +1,18 @@
module.exports = {
"env": {
"browser": true,
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"rules": {
}
};

1
src/lib/libarchive.js/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

View File

@@ -0,0 +1,3 @@
language: node_js
node_js:
- "node"

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,180 @@
// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html
module.exports = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after the first failure
// bail: false,
// Respect "browser" field in package.json when resolving modules
// browser: false,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "C:\\Users\\Nika\\AppData\\Local\\Temp\\jest",
// Automatically clear mock calls and instances between every test
// clearMocks: false,
// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: false,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: null,
// The directory where Jest should output its coverage files
// coverageDirectory: null,
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: null,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Force coverage collection from ignored files usin a array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: null,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: null,
// A set of global variables that need to be available in all test environments
// globals: {},
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "json",
// "jsx",
// "node"
// ],
// A map from regular expressions to module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "always",
// A preset that is used as a base for Jest's configuration
// preset: null,
// Run tests from one or more projects
// projects: null,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state between every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: null,
// Automatically restore mock state between every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: null,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// The path to a module that runs some code to configure or set up the testing framework before each test
// setupTestFrameworkScriptFile: null,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
testEnvironment: "node",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
testMatch: [
//"**/test/**/*.js?(x)",
"**/test/**/?(*.)+(spec|test).js?(x)"
],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// The regexp pattern Jest uses to detect test files
// testRegex: "",
// This option allows the use of a custom results processor
// testResultsProcessor: null,
// This option allows use of a custom test runner
// testRunner: "jasmine2",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",
// A map from regular expressions to paths to transformers
// transform: null,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: null,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,11 @@
emcc ../wrapper/main.c -I /usr/local/include/ -o ../build/main.o #-g4
emcc ../build/main.o /usr/local/lib/libarchive.a /usr/local/lib/liblzma.a /usr/local/lib/libssl.a /usr/local/lib/libcrypto.a \
-o ../build/libarchive.js \
-s USE_ZLIB=1 -s USE_BZIP2=1 -s MODULARIZE=1 -s EXPORT_ES6=1 -s EXPORT_NAME=libarchive -s WASM=1 -O3 -s ALLOW_MEMORY_GROWTH=1 \
-s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap","allocate","intArrayFromString"]' -s EXPORTED_FUNCTIONS=@$PWD/lib.exports -s ERROR_ON_UNDEFINED_SYMBOLS=0
cp ../build/libarchive.js ../../src/webworker/wasm-gen/
cp ../build/libarchive.wasm ../../src/webworker/wasm-gen/
echo Done

View File

@@ -0,0 +1,51 @@
FROM trzeci/emscripten
WORKDIR /opt
ADD https://github.com/libarchive/libarchive/releases/download/v3.4.0/libarchive-3.4.0.zip /opt
ADD https://github.com/madler/zlib/archive/v1.2.11.zip /opt
ADD https://netix.dl.sourceforge.net/project/lzmautils/xz-5.2.4.tar.gz /opt
ADD https://netix.dl.sourceforge.net/project/bzip2/bzip2-1.0.6.tar.gz /opt
ADD https://www.openssl.org/source/openssl-1.0.2s.tar.gz /opt
RUN unzip /opt/libarchive-3.4.0.zip && rm /opt/libarchive-3.4.0.zip && \
unzip /opt/v1.2.11.zip && rm /opt/v1.2.11.zip && \
tar xf /opt/xz-5.2.4.tar.gz && rm /opt/xz-5.2.4.tar.gz && \
tar xf /opt/bzip2-1.0.6.tar.gz && rm /opt/bzip2-1.0.6.tar.gz && \
tar xf /opt/openssl-1.0.2s.tar.gz && rm /opt/openssl-1.0.2s.tar.gz
RUN apt-get update && \
apt-get install -y locate vim file
ENV CPPFLAGS "-I/usr/local/include/ -I/opt/zlib-1.2.11 -I/opt/bzip2-1.0.6 -I/opt/openssl-1.0.2s/include -I/opt/openssl-1.0.2s/test"
ENV LDLIBS "-lz -lssl -lcrypto"
ENV LDFLAGS "-L/usr/local/lib"
# compile openSSL to LLVM
WORKDIR /opt/openssl-1.0.2s
RUN cd /opt/openssl-1.0.2s && emmake bash -c "./Configure -no-asm -no-apps no-ssl2 no-ssl3 no-hw no-deprecated shared no-dso linux-generic32" && \
sed -i 's/CC= $(CROSS_COMPILE)\/emsdk_portable\/sdk\/emcc/CC= $(CROSS_COMPILE)cc/' Makefile && \
emmake make && \
cd /usr/local/lib && \
ln -s /opt/openssl-1.0.2s/libssl.a && \
ln -s /opt/openssl-1.0.2s/libcrypto.a
# compile LZMA to LLVM
WORKDIR /opt/xz-5.2.4
RUN cd /opt/xz-5.2.4 && emconfigure ./configure --disable-assembler --enable-threads=no --enable-static=yes 2>&1 | tee conf.out && \
emmake make 2>&1 | tee make.out && emmake make install
# compile libarchive to LLVM
WORKDIR /opt/libarchive-3.4.0
RUN cd /opt/libarchive-3.4.0 && emconfigure ./configure --enable-static --disable-shared --enable-bsdtar=static --enable-bsdcat=static \
--enable-bsdcpio=static --enable-posix-regex-lib=libc \
--disable-xattr --disable-acl --without-nettle --without-lzo2 \
--without-cng --without-lz4 \
--without-xml2 --without-expat 2>&1 | tee conf.out && \
emmake make 2>&1 | tee make.out && emmake make install
#--without-openssl
#--without-bz2lib --without-iconv --without-libiconv-prefix --without-lzma
WORKDIR /var/local/lib/tools
CMD ["bash","/var/local/lib/tools/build.sh"]

View File

@@ -0,0 +1,18 @@
[
"_get_version",
"_archive_open",
"_get_next_entry",
"_get_filedata",
"_archive_close",
"_archive_entry_filetype",
"_archive_entry_pathname",
"_archive_entry_pathname_utf8",
"_archive_entry_size",
"_archive_read_data_skip",
"_archive_error_string",
"_archive_entry_is_encrypted",
"_archive_read_has_encrypted_entries",
"_archive_read_add_passphrase",
"_free",
"_malloc"
]

View File

@@ -0,0 +1,2 @@
if [ ! -f "./package.json" ]; then echo "you should run this from project root"; exit 1; fi
docker run -it -v `pwd`:/var/local libarchive-llvm

View File

@@ -0,0 +1,128 @@
#define LIBARCHIVE_STATIC
//#include "emscripten.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <archive.h>
#include <archive_entry.h>
#define EMSCRIPTEN_KEEPALIVE
EMSCRIPTEN_KEEPALIVE
const char * get_version(){
return archive_version_string();
}
EMSCRIPTEN_KEEPALIVE
void* archive_open( const void *buf, size_t size, const char * passphrase ){
struct archive *a;
int r;
a = archive_read_new();
archive_read_support_filter_all(a);
archive_read_support_format_all(a);
if( passphrase ){
archive_read_add_passphrase(a, passphrase);
}
r = archive_read_open_memory(a, buf, size);
if (r != ARCHIVE_OK){
fprintf(stderr, "Memory read error %d\n",r);
fprintf(stderr, "%s\n",archive_error_string(a));
}
return a;
}
EMSCRIPTEN_KEEPALIVE
const void* get_next_entry(void *archive){
struct archive_entry *entry;
if( archive_read_next_header(archive,&entry) == ARCHIVE_OK ){
return entry;
}else{
return NULL;
}
}
EMSCRIPTEN_KEEPALIVE
void* get_filedata(void *archive,size_t buffsize){
void *buff = malloc( buffsize );
int read_size = archive_read_data(archive,buff,buffsize);
if( read_size < 0 ){
fprintf(stderr, "Error occured while reading file");
return (void*) read_size;
}else{
return buff;
}
}
EMSCRIPTEN_KEEPALIVE
void archive_close( void *archive ){
int r = archive_read_free(archive);
if (r != ARCHIVE_OK){
fprintf(stderr, "Error read free %d\n",r);
fprintf(stderr, "%s\n",archive_error_string(archive));
}
}
/*
#define MAXBUFLEN 1000000
EMSCRIPTEN_KEEPALIVE
int main(){
char source[MAXBUFLEN + 1];
FILE *fp = fopen("addon.zip", "r");
if (fp != NULL) {
size_t newLen = fread(source, sizeof(char), MAXBUFLEN, fp);
if ( ferror( fp ) != 0 ) {
printf("Error reading file");
} else {
source[newLen++] = '\0';
void* arch = archive_open(source,newLen);
printf("arch: %d",arch);
void* entry = get_next_entry(arch);
size_t fsize = archive_entry_size(entry);
void* file = get_filedata(arch,fsize);
printf("file: %d",file);
}
fclose(fp);
}
}*/
/*
EMSCRIPTEN_KEEPALIVE
char* list_files( const void * buf, size_t size ){
printf("list_files start\n");
struct archive *a;
struct archive_entry *entry;
int r;
char* fname = NULL;
const char* tmp;
printf("variables initialized\n");
a = archive_read_new();
archive_read_support_filter_all(a);
archive_read_support_format_all(a);
printf("libarchive initialized\n");
r = archive_read_open_memory(a, buf, size);
if (r != ARCHIVE_OK){
printf("Memory read error %d\n",r);
printf("%s\n",archive_error_string(a));
exit(1);
}
printf("start read\n");
while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {
tmp = archive_entry_pathname(entry);
free(fname);
fname = malloc(strlen(tmp));
strcpy(fname,tmp);
archive_read_data_skip(a);
}
printf("finish read\n");
r = archive_read_free(a);
if (r != ARCHIVE_OK){
printf("Error read free %d\n",r);
printf("%s\n",archive_error_string(a));
exit(1);
}
return fname;
}
*/

View File

@@ -0,0 +1,2 @@
export { Archive } from './src/libarchive.js';

View File

@@ -0,0 +1,19 @@
import copy from 'rollup-plugin-copy-assets';
import { terser } from "rollup-plugin-terser";
export default {
input: 'src/webworker/worker.js',
output: [
{
file: 'dist/worker-bundle.js',
format: 'iife'
}
],
plugins: [
copy({
assets: [
'./src/webworker/wasm-gen'
],
}),
].concat( process.env.BUILD === 'production' ? [terser()] : [] ),
};

View File

@@ -0,0 +1,35 @@
/**
* Represents compressed file before extraction
*/
export class CompressedFile{
constructor(name,size,path,archiveRef){
this._name = name;
this._size = size;
this._path = path;
this._archiveRef = archiveRef;
}
/**
* file name
*/
get name(){
return this._name;
}
/**
* file size
*/
get size(){
return this._size;
}
/**
* Extract file from archive
* @returns {Promise<File>} extracted file
*/
extract(){
return this._archiveRef.extractSingleFile(this._path);
}
}

View File

@@ -0,0 +1,225 @@
import { CompressedFile } from "./compressed-file.js";
export class Archive{
/**
* Initialize libarchivejs
* @param {Object} options
*/
static init(options = {}){
Archive._options = {
workerUrl: '../dist/worker-bundle.js',
...options
};
return Archive._options;
}
/**
* Creates new archive instance from browser native File object
* @param {File} file
* @param {object} options
* @returns {Archive}
*/
static open(file, options = null){
options = options ||
Archive._options ||
Archive.init() && console.warn('Automatically initializing using options: ', Archive._options);
const arch = new Archive(file,options);
return arch.open();
}
/**
* Create new archive
* @param {File} file
* @param {Object} options
*/
constructor(file,options){
this._worker = new Worker(options.workerUrl);
this._worker.addEventListener('message', this._workerMsg.bind(this));
this._callbacks = [];
this._content = {};
this._processed = 0;
this._file = file;
}
/**
* Prepares file for reading
* @returns {Promise<Archive>} archive instance
*/
async open(){
await this._postMessage({type: 'HELLO'},(resolve,reject,msg) => {
if( msg.type === 'READY' ){
resolve();
}
});
return await this._postMessage({type: 'OPEN', file: this._file}, (resolve,reject,msg) => {
if(msg.type === 'OPENED'){
resolve(this);
}
});
}
/**
* detect if archive has encrypted data
* @returns {boolean|null} null if could not be determined
*/
hasEncryptedData(){
return this._postMessage({type: 'CHECK_ENCRYPTION'},
(resolve,reject,msg) => {
if( msg.type === 'ENCRYPTION_STATUS' ){
resolve(msg.status);
}
}
);
}
/**
* set password to be used when reading archive
*/
usePassword(archivePassword){
return this._postMessage({type: 'SET_PASSPHRASE', passphrase: archivePassword},
(resolve,reject,msg) => {
if( msg.type === 'PASSPHRASE_STATUS' ){
resolve(msg.status);
}
}
);
}
/**
* Returns object containing directory structure and file information
* @returns {Promise<object>}
*/
getFilesObject(){
if( this._processed > 0 ){
return Promise.resolve().then( () => this._content );
}
return this._postMessage({type: 'LIST_FILES'}, (resolve,reject,msg) => {
if( msg.type === 'ENTRY' ){
const entry = msg.entry;
const [ target, prop ] = this._getProp(this._content,entry.path);
if( entry.type === 'FILE' ){
target[prop] = new CompressedFile(entry.fileName,entry.size,entry.path,this);
}
return true;
}else if( msg.type === 'END' ){
this._processed = 1;
resolve(this._cloneContent(this._content));
}
});
}
getFilesArray(){
return this.getFilesObject().then( (obj) => {
return this._objectToArray(obj);
});
}
extractSingleFile(target){
return this._postMessage({type: 'EXTRACT_SINGLE_FILE', target: target},
(resolve,reject,msg) => {
if( msg.type === 'FILE' ){
const file = new File([msg.entry.fileData], msg.entry.fileName, {
type: 'application/octet-stream'
});
resolve(file);
}
}
);
}
/**
* Returns object containing directory structure and extracted File objects
* @param {Function} extractCallback
*
*/
extractFiles(extractCallback){
if( this._processed > 1 ){
return Promise.resolve().then( () => this._content );
}
return this._postMessage({type: 'EXTRACT_FILES'}, (resolve,reject,msg) => {
if( msg.type === 'ENTRY' ){
const [ target, prop ] = this._getProp(this._content,msg.entry.path);
if( msg.entry.type === 'FILE' ){
target[prop] = new File([msg.entry.fileData], msg.entry.fileName, {
type: 'application/octet-stream'
});
if (extractCallback !== undefined) {
setTimeout(extractCallback.bind(null,{
file: target[prop],
path: msg.entry.path,
}));
}
}
return true;
}else if( msg.type === 'END' ){
this._processed = 2;
this._worker.terminate();
resolve(this._cloneContent(this._content));
}
});
}
_cloneContent(obj){
if( obj instanceof File || obj instanceof CompressedFile || obj === null ) return obj;
const o = {};
for( const prop of Object.keys(obj) ){
o[prop] = this._cloneContent(obj[prop]);
}
return o;
}
_objectToArray(obj,path = ''){
const files = [];
for( const key of Object.keys(obj) ){
if( obj[key] instanceof File || obj[key] instanceof CompressedFile || obj[key] === null ){
files.push({
file: obj[key] || key,
path: path
});
}else{
files.push( ...this._objectToArray(obj[key],`${path}${key}/`) );
}
}
return files;
}
_getProp(obj,path){
const parts = path.split('/');
if( parts[parts.length -1] === '' ) parts.pop();
let cur = obj, prev = null;
for( const part of parts ){
cur[part] = cur[part] || {};
prev = cur;
cur = cur[part];
}
return [ prev, parts[parts.length-1] ];
}
_postMessage(msg,callback){
this._worker.postMessage(msg);
return new Promise((resolve,reject) => {
this._callbacks.push( this._msgHandler.bind(this,callback,resolve,reject) );
});
}
_msgHandler(callback,resolve,reject,msg){
if( msg.type === 'BUSY' ){
reject('worker is busy');
}else if( msg.type === 'ERROR' ){
reject(msg.error);
}else{
return callback(resolve,reject,msg);
}
}
_workerMsg({data: msg}){
const callback = this._callbacks[this._callbacks.length -1];
const next = callback(msg);
if( !next ){
this._callbacks.pop();
}
}
}

View File

@@ -0,0 +1,136 @@
const TYPE_MAP = {
32768: 'FILE',
16384: 'DIR',
40960: 'SYMBOLIC_LINK',
49152: 'SOCKET',
8192: 'CHARACTER_DEVICE',
24576: 'BLOCK_DEVICE',
4096: 'NAMED_PIPE',
};
export class ArchiveReader{
/**
* archive reader
* @param {WasmModule} wasmModule emscripten module
*/
constructor(wasmModule){
this._wasmModule = wasmModule;
this._runCode = wasmModule.runCode;
this._file = null;
this._passphrase = null;
}
/**
* open archive, needs to closed manually
* @param {File} file
*/
open(file){
if( this._file !== null ){
console.warn('Closing previous file');
this.close();
}
const { promise, resolve, reject } = this._promiseHandles();
this._file = file;
const reader = new FileReader();
reader.onload = () => this._loadFile(reader.result,resolve,reject);
reader.readAsArrayBuffer(file);
return promise;
}
/**
* close archive
*/
close(){
this._runCode.closeArchive(this._archive);
this._wasmModule._free(this._filePtr);
this._file = null;
this._filePtr = null;
this._archive = null;
}
/**
* detect if archive has encrypted data
* @returns {boolean|null} null if could not be determined
*/
hasEncryptedData(){
this._archive = this._runCode.openArchive( this._filePtr, this._fileLength, this._passphrase );
this._runCode.getNextEntry(this._archive);
const status = this._runCode.hasEncryptedEntries(this._archive);
if( status === 0 ){
return false;
} else if( status > 0 ){
return true;
} else {
return null;
}
}
/**
* set passphrase to be used with archive
* @param {*} passphrase
*/
setPassphrase(passphrase){
this._passphrase = passphrase;
}
/**
* get archive entries
* @param {boolean} skipExtraction
* @param {string} except don't skip this entry
*/
*entries(skipExtraction = false, except = null){
this._archive = this._runCode.openArchive( this._filePtr, this._fileLength, this._passphrase );
let entry;
while( true ){
entry = this._runCode.getNextEntry(this._archive);
if( entry === 0 ) break;
const entryData = {
size: this._runCode.getEntrySize(entry),
path: this._runCode.getEntryName(entry),
type: TYPE_MAP[this._runCode.getEntryType(entry)],
ref: entry,
};
if( entryData.type === 'FILE' ){
let fileName = entryData.path.split('/');
entryData.fileName = fileName[fileName.length - 1];
}
if( skipExtraction && except !== entryData.path ){
this._runCode.skipEntry(this._archive);
}else{
const ptr = this._runCode.getFileData(this._archive,entryData.size);
if( ptr < 0 ){
throw new Error(this._runCode.getError(this._archive));
}
entryData.fileData = this._wasmModule.HEAP8.slice(ptr,ptr+entryData.size);
this._wasmModule._free(ptr);
}
yield entryData;
}
}
_loadFile(fileBuffer,resolve,reject){
try{
const array = new Uint8Array(fileBuffer);
this._fileLength = array.length;
this._filePtr = this._runCode.malloc(this._fileLength);
this._wasmModule.HEAP8.set(array, this._filePtr);
resolve();
}catch(error){
reject(error);
}
}
_promiseHandles(){
let resolve = null,reject = null;
const promise = new Promise((_resolve,_reject) => {
resolve = _resolve;
reject = _reject;
});
return { promise, resolve, reject };
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,97 @@
/* eslint-disable no-undef */
import libarchive from './wasm-gen/libarchive.js';
export class WasmModule{
constructor(){
this.preRun = [];
this.postRun = [];
this.totalDependencies = 0;
}
print(...text){
console.log(text);
}
printErr(...text){
console.error(text);
}
initFunctions(){
this.runCode = {
// const char * get_version()
getVersion: this.cwrap('get_version', 'string', []),
// void * archive_open( const void * buffer, size_t buffer_size)
// retuns archive pointer
openArchive: this.cwrap('archive_open', 'number', ['number','number','string']),
// void * get_entry(void * archive)
// return archive entry pointer
getNextEntry: this.cwrap('get_next_entry', 'number', ['number']),
// void * get_filedata( void * archive, size_t bufferSize )
getFileData: this.cwrap('get_filedata', 'number', ['number','number']),
// int archive_read_data_skip(struct archive *_a)
skipEntry: this.cwrap('archive_read_data_skip', 'number', ['number']),
// void archive_close( void * archive )
closeArchive: this.cwrap('archive_close', null, ['number'] ),
// la_int64_t archive_entry_size( struct archive_entry * )
getEntrySize: this.cwrap('archive_entry_size', 'number', ['number']),
// const char * archive_entry_pathname( struct archive_entry * )
getEntryName: this.cwrap('archive_entry_pathname', 'string', ['number']),
// __LA_MODE_T archive_entry_filetype( struct archive_entry * )
/*
#define AE_IFMT ((__LA_MODE_T)0170000)
#define AE_IFREG ((__LA_MODE_T)0100000) // Regular file
#define AE_IFLNK ((__LA_MODE_T)0120000) // Sybolic link
#define AE_IFSOCK ((__LA_MODE_T)0140000) // Socket
#define AE_IFCHR ((__LA_MODE_T)0020000) // Character device
#define AE_IFBLK ((__LA_MODE_T)0060000) // Block device
#define AE_IFDIR ((__LA_MODE_T)0040000) // Directory
#define AE_IFIFO ((__LA_MODE_T)0010000) // Named pipe
*/
getEntryType: this.cwrap('archive_entry_filetype', 'number', ['number']),
// const char * archive_error_string(struct archive *);
getError: this.cwrap('archive_error_string', 'string', ['number']),
/*
* Returns 1 if the archive contains at least one encrypted entry.
* If the archive format not support encryption at all
* ARCHIVE_READ_FORMAT_ENCRYPTION_UNSUPPORTED is returned.
* If for any other reason (e.g. not enough data read so far)
* we cannot say whether there are encrypted entries, then
* ARCHIVE_READ_FORMAT_ENCRYPTION_DONT_KNOW is returned.
* In general, this function will return values below zero when the
* reader is uncertain or totally incapable of encryption support.
* When this function returns 0 you can be sure that the reader
* supports encryption detection but no encrypted entries have
* been found yet.
*
* NOTE: If the metadata/header of an archive is also encrypted, you
* cannot rely on the number of encrypted entries. That is why this
* function does not return the number of encrypted entries but#
* just shows that there are some.
*/
// __LA_DECL int archive_read_has_encrypted_entries(struct archive *);
entryIsEncrypted: this.cwrap('archive_entry_is_encrypted', 'number', ['number']),
hasEncryptedEntries: this.cwrap('archive_read_has_encrypted_entries', 'number', ['number']),
// __LA_DECL int archive_read_add_passphrase(struct archive *, const char *);
addPassphrase: this.cwrap('archive_read_add_passphrase', 'number', ['number','string']),
//this.stringToUTF(str), //
string: (str) => this.allocate(this.intArrayFromString(str), 'i8', 0),
malloc: this.cwrap('malloc', 'number', ['number']),
free: this.cwrap('free', null, ['number']),
};
//console.log(this.runCode.getVersion());
}
monitorRunDependencies(){}
locateFile(path /* ,prefix */ ){
return `wasm-gen/${path}`;
}
}
export function getWasmModule(cb){
libarchive( new WasmModule() ).then( (module) => {
module.initFunctions();
cb(module);
});
}

View File

@@ -0,0 +1,69 @@
import {ArchiveReader} from './archive-reader';
import {getWasmModule} from './wasm-module';
let reader = null;
let busy = false;
getWasmModule( (wasmModule) => {
reader = new ArchiveReader(wasmModule);
busy = false;
self.postMessage({type: 'READY'});
});
self.onmessage = async ({data: msg}) => {
if( busy ){
self.postMessage({ type: 'BUSY' });
return;
}
let skipExtraction = false;
busy = true;
try{
switch(msg.type){
case 'HELLO': // module will respond READY when it's ready
break;
case 'OPEN':
await reader.open(msg.file);
self.postMessage({ type: 'OPENED' });
break;
case 'LIST_FILES':
skipExtraction = true;
// eslint-disable-next-line no-fallthrough
case 'EXTRACT_FILES':
for( const entry of reader.entries(skipExtraction) ){
self.postMessage({ type: 'ENTRY', entry });
}
self.postMessage({ type: 'END' });
break;
case 'EXTRACT_SINGLE_FILE':
for( const entry of reader.entries(true,msg.target) ){
if( entry.fileData ){
self.postMessage({ type: 'FILE', entry });
}
}
break;
case 'CHECK_ENCRYPTION':
self.postMessage({ type: 'ENCRYPTION_STATUS', status: reader.hasEncryptedData() });
break;
case 'SET_PASSPHRASE':
reader.setPassphrase( msg.passphrase );
self.postMessage({ type: 'PASSPHRASE_STATUS', status: true });
break;
default:
throw new Error('Invalid Command');
}
}catch(err){
self.postMessage({
type: 'ERROR',
error: {
message: err.message,
name: err.name,
stack: err.stack
}
});
}finally{
// eslint-disable-next-line require-atomic-updates
busy = false;
}
};

View File

@@ -0,0 +1,2 @@
.vscode
.idea

View File

@@ -0,0 +1 @@
# Adjaranet plugin for Kodi

View File

@@ -0,0 +1,120 @@
import simplejson as json
from httplib2 import Http
import re
import urllib2
import sys
import urllib
import urlparse
import xbmc
import xbmcgui
import xbmcplugin
API_BASE = 'http://net.adjara.com/'
STATIC_FILES = 'http://staticnet.adjara.com/'
CATEGORY_MAP = {
'new_release': 'Search/SearchResults?ajax=1&display=15&startYear=1900&endYear=2018&offset=0&orderBy=date&order%5Border%5D=data&order%5Bdata%5D=premiere&order%5Bmeta%5D=desc',
'top_movies': 'Search/SearchResults?ajax=1&display=15&startYear=1900&endYear=2018&offset=15&orderBy=date&order%5Border%5D=data&order%5Bdata%5D=views&order%5Bmeta%5D=views-week'
}
base_url = sys.argv[0]
addon_handle = int(sys.argv[1])
args = urlparse.parse_qs(sys.argv[2][1:])
find_var_regex = re.compile(r"""movieUrlEmpty\s*=\s*[\'\"](.+)[\'\"]""")
xbmcplugin.setContent(addon_handle, 'movies')
def get_icon(movie_id):
movie_id = str(movie_id)
return STATIC_FILES + 'moviecontent/%s/covers/157x236-%s.jpg' % (movie_id,movie_id)
def get_cover(movie_id):
movie_id = str(movie_id)
return STATIC_FILES + 'moviecontent/%s/covers/1920x1080-%s.jpg' % (movie_id,movie_id)
def build_url(query):
return base_url + '?' + urllib.urlencode(query)
def add_category(label,category,iconImage = 'DefaultFolder.png', url = None):
if url is None:
url = build_url({'mode': 'category', 'category': category})
li = xbmcgui.ListItem(label, iconImage=iconImage)
xbmcplugin.addDirectoryItem(handle=addon_handle, url=url,
listitem=li, isFolder=True)
def main_screen():
add_category('Search',None,'DefaultAddonsSearch.png',build_url({'mode': 'search'}))
add_category('New Releases','new_release')
add_category('Top Movies','top_movies')
xbmcplugin.endOfDirectory(addon_handle)
def load_category(category):
cat_url = API_BASE + CATEGORY_MAP[category]
try:
(rsp_headers, json_data) = Http().request(cat_url)
data = json.loads(json_data)
for item in data['data']:
url = build_url({'mode': 'movie', 'id': item['id']})
li = xbmcgui.ListItem(item['title_en'], iconImage=item['poster'])
li.setProperty('IsPlayable', 'true')
xbmcplugin.addDirectoryItem(handle=addon_handle, url=url, listitem=li, isFolder=False)
except Exception, e:
xbmc.log('adjaranet: got http error fetching %s \n %s' % (cat_url, str(e)), xbmc.LOGWARNING)
finally:
xbmcplugin.endOfDirectory(addon_handle)
def search():
kb = xbmc.Keyboard('', 'Search for movie')
kb.doModal()
if (kb.isConfirmed()):
search_term = kb.getText()
else:
return
search_url = API_BASE + 'Home/quick_search?ajax=1&search=' + search_term
try:
(rsp_headers, json_data) = Http().request(search_url)
data = json.loads(json_data)
for item in data['movies']['data']:
url = build_url({'mode': 'movie', 'id': item['id']})
li = xbmcgui.ListItem(item['title_en'])
li.setArt({
'icon': get_icon(item['id']),
'landscape': get_cover(item['id'])
})
li.setProperty('IsPlayable', 'true')
xbmcplugin.addDirectoryItem(handle=addon_handle, url=url, listitem=li, isFolder=False)
except Exception, e:
xbmc.log('adjaranet: got http error fetching %s \n %s' % (search_url, str(e)), xbmc.LOGWARNING)
finally:
xbmcplugin.endOfDirectory(addon_handle)
def load_movie(movie_id):
script_url = API_BASE + 'Movie/main?id='+ movie_id +'&js=1'
try:
(rsp_headers, html_data) = Http().request(script_url)
match = re.search(find_var_regex,html_data)
if not match:
xbmc.log('can not find url at %s' % (script_url), xbmc.LOGWARNING)
raise Exception('url not found')
url = match.group(1).replace('{lang}','English').replace('{quality}','1500')
xbmc.log(url, xbmc.LOGWARNING)
play_item = xbmcgui.ListItem(path=url)
xbmcplugin.setResolvedUrl(addon_handle, True, listitem=play_item)
except Exception, e:
xbmc.log('adjaranet: got http error fetching %s \n %s' % (script_url, str(e)), xbmc.LOGWARNING)
mode = args.get('mode', None)
if mode is None:
main_screen()
elif mode[0] == 'category':
category = args.get('category','new_release')
load_category(category[0])
elif mode[0] == 'search':
search()
elif mode[0] == 'movie':
movie_id = args.get('id', None)
load_movie(movie_id[0])

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.addon.adjaranet.client" name="adjaranet client" version="0.0.1" provider-name="You">
<requires>
<import addon="xbmc.python" version="2.1.0"/>
<import addon="script.module.simplejson" />
<import addon="script.module.httplib2" />
</requires>
<extension point="xbmc.python.pluginsource" library="addon.py">
<provides>video</provides>
</extension>
<extension point="xbmc.addon.metadata">
<summary lang="en_GB">Adjaranet.com client</summary>
<description lang="en_GB">enable adjaranet inside kodi</description>
<disclaimer lang="en_GB"></disclaimer>
<language></language>
<platform>all</platform>
<license></license>
<forum></forum>
<website></website>
<email></email>
<source></source>
<news></news>
<assets>
<icon></icon>
<fanart></fanart>
<banner></banner>
<clearlogo></clearlogo>
<screenshot></screenshot>
</assets>
</extension>
</addon>

View File

@@ -0,0 +1,9 @@
module.exports.checksum = {
'.gitignore':'d1e8d4fa856e17b2ad54a216aae527a880873df76cc30a85d6ba6b32d2ee23cc',
'addon':{
'addon.py':'e0ab20fe5fd7ab5c2b38511d81d93b9cb6246e300d0893face50e8a5b9485b90',
'addon.xml':'d26a8bdf02e7ab2eaeadf2ab603a1d11b2a5bfe57a6ac672d1a1c4940958eba8'
},
'README.md':'b4555fd8dd6e81599625c1232e58d5e09fc36f3f6614bf792a6978b30cfe65bb'
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html>
<head>
<title>test webworker</title>
</head>
<body>
<input type="file" id="file" />
<script type="module" >
function hex(buffer) {
const hexCodes = [];
const view = new DataView(buffer);
for (let i = 0; i < view.byteLength; i += 4) {
const value = view.getUint32(i)
const stringValue = value.toString(16)
const padding = '00000000'
const paddedValue = (padding + stringValue).slice(-padding.length)
hexCodes.push(paddedValue);
}
return hexCodes.join("");
}
function getChecksum(file){
return new Promise((resolve,reject) => {
try{
const reader = new FileReader();
reader.onload = function() {
crypto.subtle.digest("SHA-256", reader.result).then(function (hash) {
resolve(hex(hash));
});
}
reader.readAsArrayBuffer(file);
}catch(err){
reject(err);
}
});
}
function finish(){
const d = document.createElement('div');
d.setAttribute('id','done');
d.textContent = 'Done.';
document.body.appendChild(d);
}
async function fileChecksums(obj){
for( const [key,val] of Object.entries(obj) ){
obj[key] = val instanceof File ?
await getChecksum(val) : await fileChecksums(val);
}
return obj;
}
import {Archive} from '../../src/libarchive.js';
Archive.init({
workerUrl: '../../dist/worker-bundle.js'
});
window.Archive = Archive;
document.getElementById('file').addEventListener('change', async (e) => {
let obj = null, encEntries = false;
try{
const file = e.currentTarget.files[0];
const archive = await Archive.open(file);
encEntries = await archive.hasEncryptedData();
await archive.usePassword("nika");
obj = await archive.extractFiles();
obj = await fileChecksums(obj);
}catch(err){
console.error(err);
}finally{
window.obj = {files: obj, encrypted: encEntries};
finish();
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html>
<head>
<title>test webworker</title>
</head>
<body>
<input type="file" id="file" />
<script type="module" >
function hex(buffer) {
const hexCodes = [];
const view = new DataView(buffer);
for (let i = 0; i < view.byteLength; i += 4) {
const value = view.getUint32(i)
const stringValue = value.toString(16)
const padding = '00000000'
const paddedValue = (padding + stringValue).slice(-padding.length)
hexCodes.push(paddedValue);
}
return hexCodes.join("");
}
function getChecksum(file){
return new Promise((resolve,reject) => {
try{
const reader = new FileReader();
reader.onload = function() {
crypto.subtle.digest("SHA-256", reader.result).then(function (hash) {
resolve(hex(hash));
});
}
reader.readAsArrayBuffer(file);
}catch(err){
reject(err);
}
});
}
function finish(){
const d = document.createElement('div');
d.setAttribute('id','done');
d.textContent = 'Done.';
document.body.appendChild(d);
}
async function fileChecksums(obj){
for( const [key,val] of Object.entries(obj) ){
obj[key] = val instanceof File ?
await getChecksum(val) : await fileChecksums(val);
}
return obj;
}
import {Archive} from '../../src/libarchive.js';
Archive.init({
workerUrl: '../../dist/worker-bundle.js'
});
window.Archive = Archive;
document.getElementById('file').addEventListener('change', async (e) => {
let obj = null;
try{
const file = e.currentTarget.files[0];
const archive = await Archive.open(file);
//console.log( await archive.getFilesObject() );
//console.log( await archive.getFilesArray() );
obj = await archive.extractFiles();
//console.log( await archive.getFilesObject() );
//console.log( await archive.getFilesArray() );
obj = await fileChecksums(obj);
}catch(err){
console.error(err);
}finally{
window.obj = obj;
finish();
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html>
<head>
<title>test webworker</title>
</head>
<body>
<input type="file" id="file" />
<script type="module" >
function hex(buffer) {
const hexCodes = [];
const view = new DataView(buffer);
for (let i = 0; i < view.byteLength; i += 4) {
const value = view.getUint32(i)
const stringValue = value.toString(16)
const padding = '00000000'
const paddedValue = (padding + stringValue).slice(-padding.length)
hexCodes.push(paddedValue);
}
return hexCodes.join("");
}
function getChecksum(file){
return new Promise((resolve,reject) => {
try{
const reader = new FileReader();
reader.onload = function() {
crypto.subtle.digest("SHA-256", reader.result).then(function (hash) {
resolve(hex(hash));
});
}
reader.readAsArrayBuffer(file);
}catch(err){
reject(err);
}
});
}
function finish(){
const d = document.createElement('div');
d.setAttribute('id','done');
d.textContent = 'Done.';
document.body.appendChild(d);
}
async function fileChecksums(obj){
for( const [key,val] of Object.entries(obj) ){
obj[key] = val instanceof File ?
await getChecksum(val) : await fileChecksums(val);
}
return obj;
}
import {Archive} from '../../src/libarchive.js';
Archive.init({
workerUrl: '../../dist/worker-bundle.js'
});
window.Archive = Archive;
document.getElementById('file').addEventListener('change', async (e) => {
let objAfter,objBefore,fileObj;
try{
const file = e.currentTarget.files[0];
const archive = await Archive.open(file);
const files = await archive.getFilesArray();
fileObj = await files[0].file.extract();
}catch(err){
console.error(err);
}finally{
window.obj = await getChecksum(fileObj);
finish();
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,36 @@
/* eslint-disable no-undef */
const {checksum} = require('../checksum');
const {navigate,inputFile,response,setup,cleanup} = require('../testutils');
let browser,page;
beforeAll(async () => {
let tmp = await setup();
browser = tmp.browser;
page = tmp.page;
});
describe("Extract 7Z files with various compressions", () => {
test("Extract 7Z with LZMA", async () => {
await navigate(page);
await inputFile('archives/7z/lzma.7z',page);
const files = await response(page);
expect(files).toEqual(checksum);
}, 16000);
test("Extract 7Z with LZMA2", async () => {
await navigate(page);
await inputFile('archives/7z/lzma2.7z',page);
const files = await response(page);
expect(files).toEqual(checksum);
}, 16000);
test("Extract 7Z with BZIP2", async () => {
await navigate(page);
await inputFile('archives/7z/bzip2.7z',page);
const files = await response(page);
expect(files).toEqual(checksum);
}, 16000);
});
afterAll(() => {
cleanup(browser);
});

View File

@@ -0,0 +1,30 @@
/* eslint-disable no-undef */
const {checksum} = require('../checksum');
const {navigate,inputFile,response,setup,cleanup} = require('../testutils');
let browser,page;
beforeAll(async () => {
let tmp = await setup();
browser = tmp.browser;
page = tmp.page;
});
describe("Extract RAR files", () => {
test("Extract RAR v4", async () => {
await navigate(page);
await inputFile('archives/rar/test-v4.rar',page);
const files = await response(page);
expect(files).toEqual(checksum);
}, 16000);
test("Extract RAR v5", async () => {
await navigate(page);
await inputFile('archives/rar/test-v5.rar',page);
const files = await response(page);
expect(files).toEqual(checksum);
}, 16000);
});
afterAll(() => {
cleanup(browser);
});

View File

@@ -0,0 +1,42 @@
/* eslint-disable no-undef */
const {checksum} = require('../checksum');
const {navigate,inputFile,response,setup,cleanup} = require('../testutils');
let browser,page;
beforeAll(async () => {
let tmp = await setup();
browser = tmp.browser;
page = tmp.page;
});
describe("Extract TAR files with various compressions", () => {
test("Extract TAR without compression", async () => {
await navigate(page);
await inputFile('archives/tar/test.tar',page);
const files = await response(page);
expect(files).toEqual(checksum);
}, 16000);
test("Extract TAR BZIP2", async () => {
await navigate(page);
await inputFile('archives/tar/test.tar.bz2',page);
const files = await response(page);
expect(files).toEqual(checksum);
}, 16000);
test("Extract TAR GZIP", async () => {
await navigate(page);
await inputFile('archives/tar/test.tar.gz',page);
const files = await response(page);
expect(files).toEqual(checksum);
}, 16000);
test("Extract TAR LZMA2", async () => {
await navigate(page);
await inputFile('archives/tar/test.tar.xz',page);
const files = await response(page);
expect(files).toEqual(checksum);
}, 16000);
});
afterAll(() => {
cleanup(browser);
});

Some files were not shown because too many files have changed in this diff Show More