Merge pull request #82 from zelfroster/download-chunks

feat: view actual download chunks and fixed download actions
This commit is contained in:
csoler 2023-08-14 16:59:33 +02:00 committed by GitHub
commit fac0e3ccc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 192 additions and 261 deletions

View File

@ -723,7 +723,7 @@ const PostView = () => {
post.mFiles.map((file) =>
m('tr', [
m('td', file.mName),
m('td', util.formatbytes(file.mSize.xint64)),
m('td', rs.formatBytes(file.mSize.xint64)),
m(
'button',
{

View File

@ -180,19 +180,8 @@ const FilesTable = () => {
};
};
function formatbytes(bytes, decimals = 2) {
// takes in size returns a string (size)(kb/gb/tb)
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
const ChannelTable = () => {
return {
oninit: (v) => {},
view: (v) => m('table.channels', [m('tr', [m('th', 'Channel Name')]), v.children]),
};
};
@ -239,7 +228,6 @@ module.exports = {
Data,
SearchBar,
ChannelSummary,
formatbytes,
DisplayChannelsFromList,
updatedisplaychannels,
ChannelTable,

View File

@ -7,6 +7,7 @@ const Downloads = {
strategies: {},
statusMap: {},
hashes: [],
chunksMap: {},
loadStrategy() {
rs.rsJsonApiRequest('/rsFiles/FileDownloads', {}, (d) =>
@ -19,22 +20,29 @@ const Downloads = {
},
async loadHashes() {
await rs.rsJsonApiRequest('/rsFiles/FileDownloads', {}, (d) => (Downloads.hashes = d.hashs));
await rs
.rsJsonApiRequest('/rsFiles/FileDownloads', {}, (d) => (Downloads.hashes = d.hashs))
.then(() => {
Downloads.hashes.forEach((hash) => {
rs.rsJsonApiRequest('/rsFiles/FileDownloadChunksDetails', {
hash,
}).then((res) => (this.chunksMap[hash] = res.body.info));
});
});
},
async loadStatus() {
await Downloads.loadHashes();
const fileKeys = Object.keys(Downloads.statusMap);
if (Downloads.hashes !== undefined && Downloads.hashes.length !== fileKeys.length) {
// New file added
if (Downloads.hashes.length > fileKeys.length) {
// New file added
const newHashes = util.compareArrays(Downloads.hashes, fileKeys);
for (const hash of newHashes) {
Downloads.updateFileDetail(hash, true);
}
}
// Existing file removed
else {
} else {
// Existing file removed
const oldHashes = util.compareArrays(fileKeys, Downloads.hashes);
for (const hash of oldHashes) {
delete Downloads.statusMap[hash];
@ -125,13 +133,7 @@ const NewFileDialog = () => {
m('input[type=text][name=fileurl]', {
onchange: (e) => (url = e.target.value),
}),
m(
'button',
{
onclick: () => addFile(url),
},
'Add'
),
m('button', { onclick: () => addFile(url) }, 'Add'),
],
};
};
@ -140,27 +142,17 @@ const Component = () => {
return {
oninit: () => {
Downloads.loadStrategy();
rs.setBackgroundTask(Downloads.loadStatus, 1000, () => {
return m.route.get() === '/files/files';
});
rs.setBackgroundTask(Downloads.loadStatus, 1000, () => m.route.get() === '/files/files');
Downloads.resetSearch();
},
view: () => [
m('.widget__body-heading', [
m('h3', 'Downloads (' + (Downloads.hashes && Downloads.hashes.length) + ' files)'),
m('h3', `Downloads (${Downloads.hashes ? Downloads.hashes.length : 0} files)`),
m('.action', [
m('button', { onclick: () => widget.popupMessage(m(NewFileDialog)) }, 'Add new file'),
m(
'button',
{
onclick: () => widget.popupMessage(m(NewFileDialog)),
},
'Add new file'
),
m(
'button',
{
onclick: () => rs.rsJsonApiRequest('/rsFiles/FileClearCompleted'),
},
{ onclick: () => rs.rsJsonApiRequest('/rsFiles/FileClearCompleted') },
'Clear completed'
),
]),
@ -173,7 +165,7 @@ const Component = () => {
strategy: Downloads.strategies[hash],
direction: 'down',
transferred: Downloads.statusMap[hash].transfered.xint64,
parts: [],
chunksInfo: Downloads.chunksMap[hash],
})
),
]),

View File

@ -0,0 +1,24 @@
const m = require('mithril');
const rs = require('rswebui');
const futil = require('files/files_util');
const fileProxyObj = futil.createProxy({}, () => {
m.redraw();
});
rs.events[rs.RsEventsType.FILE_TRANSFER] = {
handler: (event) => {
console.log('search results : ', event);
// if request item doesn't already exists in Object then create new item
if (!Object.prototype.hasOwnProperty.call(fileProxyObj, event.mRequestId)) {
fileProxyObj[event.mRequestId] = [];
}
fileProxyObj[event.mRequestId].push(...event.mResults);
},
};
module.exports = {
fileProxyObj,
};

View File

@ -1,6 +1,7 @@
const m = require('mithril');
const rs = require('rswebui');
const futil = require('files/files_util');
const fproxy = require('files/files_proxy');
const widget = require('widgets');
let matchString = '';
@ -66,7 +67,7 @@ const Layout = () => {
),
]),
m('div.file-search-container__results', [
Object.keys(futil.proxyObj).length === 0
Object.keys(fproxy.fileProxyObj).length === 0
? m('h5.bold', 'Results')
: m('table.results-container', [
m(
@ -80,13 +81,13 @@ const Layout = () => {
),
m(
'tbody.results',
futil.proxyObj[currentItem.slice(1)] === undefined &&
futil.proxyObj[currentItem.slice(1)].length === 0
fproxy.fileProxyObj[currentItem.slice(1)] === undefined &&
fproxy.fileProxyObj[currentItem.slice(1)].length === 0
? 'Fetching Results...'
: futil.proxyObj[currentItem.slice(1)].map((item) =>
: fproxy.fileProxyObj[currentItem.slice(1)].map((item) =>
m('tr', [
m('td.results__name', [m('i.fas.fa-file'), m('span', item.fName)]),
m('td.results__size', futil.makeFriendlyUnit(item.fSize.xint64)),
m('td.results__size', rs.formatBytes(item.fSize.xint64)),
m('td.results__hash', item.fHash),
m(
'td.results__download',

View File

@ -51,22 +51,6 @@ const createProxy = (obj, onChange) => {
});
};
const proxyObj = createProxy({}, () => {
m.redraw();
});
function makeFriendlyUnit(bytes) {
let cnt = bytes;
for (const s of ['', 'k', 'M', 'G']) {
if (cnt < 1000) {
return cnt.toFixed(1) + ' ' + s + 'B';
} else {
cnt = cnt / 1024;
}
}
return cnt.toFixed(1) + 'TB';
}
function calcRemainingTime(bytes, rate) {
if (rate <= 0 || bytes < 1) {
return '--';
@ -91,29 +75,21 @@ function calcRemainingTime(bytes, rate) {
}
}
async function fileAction(hash, action) {
let actionHeader = '';
function fileAction(hash, action) {
const jsonParams = {
hash,
flags: 0,
};
switch (action) {
case 'cancel':
actionHeader = '/rsFiles/FileCancel';
break;
case 'pause':
actionHeader = '/rsFiles/FileControl';
jsonParams.flags = RS_FILE_CTRL_PAUSE;
break;
case 'resume':
actionHeader = '/rsFiles/FileControl';
jsonParams.flags = RS_FILE_CTRL_START;
break;
case 'force_check':
actionHeader = '/rsFiles/FileControl';
jsonParams.flags = RS_FILE_CTRL_FORCE_CHECK;
break;
@ -121,182 +97,115 @@ async function fileAction(hash, action) {
console.error('Unknown action in Downloads.control()');
return;
}
const res = await rs.rsJsonApiRequest(actionHeader, jsonParams, () => {});
return res.body.retval;
}
function actionButton(file, action) {
switch (action) {
case 'resume':
return m(
'button',
{
title: 'resume',
onclick() {
fileAction(file.hash, 'resume');
},
},
m('i.fas.fa-play')
);
case 'pause':
return m(
'button',
{
title: 'pause',
onclick() {
fileAction(file.hash, 'pause');
},
},
m('i.fas.fa-pause')
);
case 'cancel':
return m(
'button.red',
{
title: 'cancel',
onclick() {
widget.popupMessage(
m('Cancelpop', [
m('p', 'Are you sure you want to cancel download?'),
m(
'button',
{
onclick: async () => {
const res = await fileAction(file.hash, 'cancel');
if (res) {
widget.popupMessage(m('p', 'Download Cancelled Successfully'));
} else {
widget.popupMessage(m('p', 'Download Cancel Failed'));
}
m.redraw();
},
},
'Cancel'
),
])
);
// fileAction(file.hash, 'cancel');
},
},
m('i.fas.fa-times')
);
}
rs.rsJsonApiRequest('/rsFiles/FileControl', jsonParams);
}
const ProgressBar = () => {
return {
view: (v) =>
m('.progressbar', [
m('span.progressbar-status', {
style: {
width: v.attrs.rate + '%',
},
}),
m('span.progressbar-percent', v.attrs.rate.toPrecision(3) + '%'),
m('.progress-bar-chunks', [
v.attrs.chunksInfo.chunks.map((item) => m(`span.chunk[data-chunkVal=${item}]`)),
m('span.progress-bar-chunks__percent', v.attrs.rate.toPrecision(3) + '%'),
]),
};
};
const chunkStrats = {
0: 'Streaming', // CHUNK_STRATEGY_STREAMING
1: 'Random', // CHUNK_STRATEGY_RANDOM
2: 'Progressive', // CHUNK_STRATEGY_PROGRESSIVE
};
// rstypes.h :: 366
const File = () => {
let chunkStrat;
const chunkStrats = {
// rstypes.h :: 366
0: 'Streaming', // CHUNK_STRATEGY_STREAMING
1: 'Random', // CHUNK_STRATEGY_RANDOM
2: 'Progressive', // CHUNK_STRATEGY_PROGRESSIVE
};
function fileCancel(hash) {
rs.rsJsonApiRequest('/rsFiles/FileCancel', { hash }).then((res) =>
widget.popupMessage(m('p', `Download Cancel ${res ? 'Successful' : 'Failed'}`))
);
}
function cancelFileDownload(hash) {
widget.popupMessage([
m('p', 'Are you sure you want to cancel download?'),
m('button', { onclick: () => fileCancel(hash) }, 'Cancel'),
]);
}
function actionButton(file, action) {
return m(
'button',
{ title: action, onclick: () => fileAction(file.hash, action) },
m(`i.fas.fa-${action === 'resume' ? 'play' : action}`)
);
}
return {
oninit: async (v) => {
chunkStrat = await v.attrs.strategy;
},
view: (v) => {
chunkStrat = v.attrs && v.attrs.strategy;
return m(
'.file-view',
{
key: v.attrs.info.hash,
style: {
display: v.attrs.info.isSearched ? 'block' : 'none',
},
},
[
m('.file-view__heading', [
m('h6', v.attrs.info.fname),
!(v.attrs.direction === 'up') && [
const { info, direction, transferred, chunksInfo } = v.attrs;
function changeChunkStrategy(e) {
chunkStrat = e.target.selectedIndex;
rs.rsJsonApiRequest('/rsFiles/setChunkStrategy', {
hash: info.hash,
newStrategy: chunkStrat,
});
}
return m('.file-view', { style: { display: info.isSearched ? 'block' : 'none' } }, [
m('.file-view__heading', [
m('h6', info.fname),
chunkStrat !== undefined &&
direction === 'down' && [
m('.file-view__heading-chunk', [
m('label[for=chunkTag]', 'Set Chunk Strategy: '),
m(
'select[id=chunkTag]',
{
value: chunkStrat,
onchange: (e) => {
chunkStrat = e.target.selectedIndex;
rs.rsJsonApiRequest('/rsFiles/setChunkStrategy', {
hash: v.attrs.info.hash,
newStrategy: chunkStrat,
});
},
},
[
Object.keys(chunkStrats).map((opt) =>
m('option', { value: opt }, chunkStrats[opt])
),
]
),
m('select[id=chunkTag]', { value: chunkStrat, onchange: changeChunkStrategy }, [
Object.keys(chunkStrats).map((strat) =>
m('option', { value: strat }, chunkStrats[strat])
),
]),
]),
],
]),
m('.file-view__body', [
m(
'.file-view__body-progress',
!(v.attrs.direction === 'up') &&
m(ProgressBar, {
rate: (v.attrs.transferred / v.attrs.info.size.xint64) * 100,
})
),
m('.file-view__body-details', [
m('.file-view__body-details-stat', [
m('span', m('i.fas.fa-download'), makeFriendlyUnit(v.attrs.transferred)),
m('span', m('i.fas.fa-file'), makeFriendlyUnit(v.attrs.info.size.xint64)),
m(
'span',
m('i.fas.fa-arrow-circle-' + v.attrs.direction),
makeFriendlyUnit(v.attrs.info.tfRate * 1024) + '/s'
),
!(v.attrs.direction === 'up') &&
m('span', { title: 'time remaining' }, [
m('i.fas.fa-clock'),
calcRemainingTime(
v.attrs.info.size.xint64 - v.attrs.transferred,
v.attrs.info.tfRate
),
]),
m(
'span',
{ title: 'peers' },
[m('i.fas.fa-users'), v.attrs.info.peers.length],
v.attrs.parts.reduce((a, e) => [...a, ' - ' + makeFriendlyUnit(e)], [])
),
]),
m('.file-view__body', [
m(
'.file-view__body-progress',
direction === 'down' &&
m(ProgressBar, { rate: (transferred / info.size.xint64) * 100, chunksInfo })
),
m('.file-view__body-details', [
m('.file-view__body-details-stat', [
m('span', { title: 'downloaded size' }, [
m('i.fas.fa-download'),
rs.formatBytes(transferred),
]),
m(
'.file-view__body-details-action',
!(v.attrs.info.downloadStatus === FT_STATE_COMPLETE) && [
actionButton(
v.attrs.info,
v.attrs.info.downloadStatus === FT_STATE_PAUSED ? 'resume' : 'pause'
),
actionButton(v.attrs.info, 'cancel'),
]
),
m('span', { title: 'total size' }, [
m('i.fas.fa-file'),
rs.formatBytes(info.size.xint64),
]),
m('span', { title: 'speed' }, [
m(`i.fas.fa-arrow-circle-${direction}`),
`${rs.formatBytes(info.tfRate * 1024)}/s`,
]),
direction === 'down' &&
m('span', { title: 'time remaining' }, [
m('i.fas.fa-clock'),
calcRemainingTime(info.size.xint64 - transferred, info.tfRate),
]),
m('span', { title: 'peers' }, [m('i.fas.fa-users'), info.peers.length]),
]),
m(
'.file-view__body-details-action',
info.downloadStatus !== FT_STATE_COMPLETE && [
actionButton(info, info.downloadStatus === FT_STATE_PAUSED ? 'resume' : 'pause'),
m(
'button.red',
{ title: 'cancel', onclick: () => cancelFileDownload(info.hash) },
m('i.fas.fa-times')
),
]
),
]),
]
);
]),
]);
},
};
};
@ -310,11 +219,8 @@ const SearchBar = () => {
oninput: (e) => {
searchString = e.target.value.toLowerCase();
for (const hash in v.attrs.list) {
if (v.attrs.list[hash].fname.toLowerCase().indexOf(searchString) > -1) {
v.attrs.list[hash].isSearched = true;
} else {
v.attrs.list[hash].isSearched = false;
}
v.attrs.list[hash].isSearched =
v.attrs.list[hash].fname.toLowerCase().indexOf(searchString) > -1;
}
},
}),
@ -352,14 +258,6 @@ const FriendsFilesTable = () => {
]),
};
};
function formatbytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
module.exports = {
RS_FILE_CTRL_PAUSE,
@ -376,12 +274,10 @@ module.exports = {
RS_FILE_REQ_ANONYMOUS_ROUTING,
RS_FILE_HINTS_REMOTE,
RS_FILE_HINTS_LOCAL,
makeFriendlyUnit,
File,
SearchBar,
compareArrays,
MyFilesTable,
FriendsFilesTable,
formatbytes,
proxyObj,
createProxy,
};

View File

@ -73,7 +73,7 @@ function displayfiles() {
? nameOfId + ' (' + parStruct.details.name.slice(0, 8) + '...)'
: parStruct.details.name
),
m('td', util.formatbytes(parStruct.details.size.xint64)),
m('td', rs.formatBytes(parStruct.details.size.xint64)),
isFile &&
m(
'td',

View File

@ -48,7 +48,7 @@ function displayfiles() {
},
parStruct.details.name
),
m('td', util.formatbytes(parStruct.details.size.xint64)),
m('td', rs.formatBytes(parStruct.details.size.xint64)),
]),
parStruct.showChild &&
childrenList.map((child) =>

View File

@ -1,5 +1,4 @@
const m = require('mithril');
const futil = require('files/files_util');
const RsEventsType = {
NONE: 0, // Used internally to detect invalid event type passed
@ -170,18 +169,6 @@ function deeperIfExist(map, key, action) {
const eventQueue = {
events: {
[RsEventsType.FILE_TRANSFER]: {
handler: (event) => {
console.log('search results : ', event);
// if request item doesn't already exists in Object then create new item
if (!Object.prototype.hasOwnProperty.call(futil.proxyObj, event.mRequestId)) {
futil.proxyObj[event.mRequestId] = [];
}
futil.proxyObj[event.mRequestId].push(...event.mResults);
},
},
[RsEventsType.CHAT_MESSAGE]: {
// Chat-Messages
types: {
@ -336,12 +323,23 @@ function logon(loginHeader, displayAuthError, displayErrorMessage, successful) {
});
}
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
module.exports = {
rsJsonApiRequest,
setKeys,
setBackgroundTask,
logon,
events: eventQueue.events,
RsEventsType,
userList,
loginKey,
formatBytes,
};

View File

@ -1,6 +1,6 @@
@use '../abstracts/colors' as *;
.progressbar {
.progress-bar {
width: 100%;
height: 2rem;
position: relative;
@ -8,7 +8,7 @@
background-color: $light-color;
border-radius: 20px;
overflow: hidden;
&-status {
&__status {
position: absolute;
top: 0;
left: 0;
@ -16,11 +16,43 @@
color: $dark-color;
background-color: $primary-color;
}
&-percent {
&__percent {
position: absolute;
inset: 0;
margin: auto;
width: fit-content;
height: fit-content;
}
&-chunks {
position: relative;
margin-top: 0.5rem;
width: 100%;
height: 2rem;
display: flex;
border-radius: 0.25rem;
overflow: hidden;
background-color: $light-color;
& .chunk {
width: 100%;
&[data-chunkVal='0'] {
background-color: transparentize($primary-light-color, 0.8);
}
&[data-chunkVal='1'] {
background-color: $red-color;
}
&[data-chunkVal='2'] {
background-color: $primary-color;
}
&[data-chunkVal='3'] {
background-color: $golden-yellow-color;
}
}
&__percent {
position: absolute;
inset: 0;
margin: auto;
width: fit-content;
height: fit-content;
}
}
}

File diff suppressed because one or more lines are too long