Implemented icon upload

canary
WolverinDEV 2019-05-24 22:14:02 +02:00
parent 09b13e0b08
commit 22e93fb317
12 changed files with 826 additions and 21 deletions

View File

@ -1,4 +1,7 @@
# Changelog:
* **24.05.19**
- Implemented icon upload
* **21.05.19**
- Restructured project
- Redesigned the audio input API (required for the client)

View File

@ -147,4 +147,211 @@
margin-left: 10px;
}
}
}
.modal-icon-upload {
user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
min-width: 300px;
display: flex;
flex-direction: column;
.container-select {
display: flex;
flex-direction: row;
justify-content: stretch;
.container-icons {
flex-grow: 1;
flex-shrink: 1;
width: min-content;
min-width: 150px;
min-height: 200px;
margin-right: 5px;
border: gray solid 1px;
border-radius: 2px;
display: block;
.icon-container {
display: inline-block;
height: 18px;
width: 18px;
image {
height: 16px;
width: 16px;
}
margin: 1px;
padding: 1px;
&:hover {
padding: 0;
border: 1px solid black;
}
&.selected {
padding: 0;
border: 1px solid red;
}
}
}
.container-buttons {
flex-grow: 1;
flex-shrink: 2;
min-width: 50px;
max-width: 200px;
display: flex;
flex-direction: column;
justify-content: space-between;
.buttons-manage {
display: flex;
flex-direction: column;
justify-content: flex-start;
}
}
}
.container-upload {
margin-top: 5px;
border-top: 1px solid darkgray;
padding-top: 5px;
.container-error, .container-success {
width: 100%;
min-height: 60px;
display: inline-block;
.error-message, .message {
display: inline-block;
}
button {
float: right;
display: inline-block;
}
}
.container-success {
margin-top: 5px;
}
.container-info {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.container-process {
width: 100%;
min-height: 100px;
border: gray solid 1px;
border-radius: 2px;
.upload-entry {
display: flex;
flex-direction: row;
justify-content: stretch;
.container-icon {
height: 16px;
width: 16px;
flex-grow: 0;
flex-shrink: 0;
margin: 1px 1px 1px 4px;
align-self: center;
> img {
height: 16px;
width: 16px;
vertical-align: unset;
}
}
.progress {
position: relative;
flex-grow: 1;
flex-shrink: 1;
margin: 2px 5px 2px 3px;
height: 16px;
.progress-bar {
height: 100%;
}
.progress-message {
align-self: center;
text-align: center;
position: absolute;
width: 100%;
}
}
}
}
}
input[type="file"] {
display: none;
}
}
@media screen and (max-width: 650px) {
.modal-icon-upload {
.container-select {
flex-direction: column;
.container-icons {
width: 100%;
margin-right: 0;
}
.container-buttons {
max-width: unset;
margin-top: 5px;
> button {
width: 100%;
}
.buttons-manage {
display: flex;
flex-direction: row;
justify-content: stretch;
> button {
width: 50%;
flex-grow: 1;
flex-shrink: 1;
min-width: 0;
}
}
}
}
.container-upload {
display: flex;
flex-direction: column;
justify-content: flex-start;
}
}
}

View File

@ -207,7 +207,7 @@
<script class="jsrender-template" id="tmpl_select_info" type="text/html">
<div class="select_info" style="width: 100%; max-width: 100%">
<button type="button" class="close" aria-label="Close">
<button type="button" class="close button-modal-close" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<div class="container-banner"></div>
@ -242,11 +242,9 @@
<div class="modal-content" style="{{if full_size}}flex-grow: 1{{/if}}">
<div class="modal-header {{if header_class}}{{:header_class}}{{/if}}">
<node key="modal_header"></node>
{{if closeable}}
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<button type="button" class="close button-modal-close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
{{/if}}
</div>
<div class="modal-body">
<node key="modal_body"></node>
@ -267,11 +265,9 @@
<div class="modal-content">
<div class="modal-header modal-header-input">
<node key="modal_header"></node>
{{if closeable}}
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<button type="button" class="close button-modal-close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
{{/if}}
</div>
<div class="modal-body modal-body-input">
<node key="question"></node>
@ -3097,7 +3093,7 @@
<div class="header">{{tr "Remote" /}}</div>
<div class="content">
<div class="container-icons-list">
<div class="container-icons-remote {{if enable_select}}icon-select{{/if}}"></div>
<div class="container-icons-remote {{if enable_select || enable_delete}}icon-select{{/if}}"></div>
<div class="container-loading">
<a>{{tr "loading..." /}}</a>
</div>
@ -3108,12 +3104,14 @@
<a class="error-message">{{ŧr "An error occured" /}}</a>
</div>
</div>
<!--
<div class="container-buttons">
{{if enable_upload}}
<button class="btn btn-success button-upload">{{tr "Upload" /}}</button>
<button class="btn btn-danger button-upload">{{tr "Delete" /}}</button>
{{/if}}
{{if enable_delete}}
<button class="btn btn-danger button-delete">{{tr "Delete" /}}</button>
{{/if}}
</div>
-->
</div>
</div>
<div class="group_box">
@ -3136,6 +3134,38 @@
</div>
</script>
<script class="jsrender-template" id="tmpl_icon_upload" type="text/html">
<div class="modal-icon-upload">
<div class="container-select">
<div class="container-icons"> </div>
<div class="container-buttons">
<div class="buttons-manage">
<button class="btn btn-primary btn-raised button-add">{{tr "Add icon" /}}</button>
<button class="btn btn-danger button-remove">{{tr "Remove selected" /}}</button>
</div>
<button class="btn btn-primary btn-raised button-upload"></button>
<input type="file" class="input-file-upload" multiple/>
</div>
</div>
<div class="container-upload">
<div class="container-error alert alert-danger">
<div class="error-message">You're not connected. Failed to upload icons</div>
<button type="button" class="btn btn-danger btn-raised button-upload-abort">{{tr "abort" /}}</button>
</div>
<div class="container-process"></div>
<div class="container-info">
<div class="container-info-uploaded">{{tr "Uploaded icons (total | successfully | error): " /}}</div>
<div class="uploaded-statistics"></div>
</div>
<div class="container-success alert alert-success">
<div class="message">Uploaded 10 icons successfully</div>
<button type="button" class="btn btn-success btn-raised button-upload-abort">{{tr "okey" /}}</button>
</div>
</div>
</div>
</script>
<script class="jsrender-template" id="tmpl_avatar_list" type="text/html">
<div class="modal-avatar-list">
<div class="container-list">

View File

@ -56,8 +56,8 @@ namespace transfer {
export function spawn_download_transfer(key: DownloadKey) : DownloadTransfer {
return new RequestFileDownload(key);
}
export function spawn_upload_transfer(key: DownloadKey) : DownloadTransfer {
return new RequestFileDownload(key);
export function spawn_upload_transfer(key: UploadKey) : RequestFileUpload {
return new RequestFileUpload(key);
}
}
@ -353,6 +353,28 @@ class FileManager extends connection.AbstractCommandHandler {
(transfer["_callback"] as (val: transfer.UploadKey) => void)(transfer);
this.pending_upload_requests.remove(transfer);
}
/** File management **/
async delete_file(props: {
name: string,
path?: string;
cid?: number;
cpw?: string;
}) : Promise<void> {
if(!props.name)
throw "invalid name!";
try {
await this.handle.serverConnection.send_command("ftdeletefile", {
cid: props.cid || 0,
cpw: props.cpw,
path: props.path || "",
name: props.name
})
} catch(error) {
throw error;
}
}
}
class Icon {
@ -386,8 +408,19 @@ function media_image_type(type: ImageType, file?: boolean) {
}
}
function image_type(base64: string) {
const bin = atob(base64);
function image_type(base64: string | ArrayBuffer) {
const ab2str10 = () => {
const buf = new Uint8Array(base64 as ArrayBuffer);
if(buf.byteLength < 10)
return "";
let result = "";
for(let index = 0; index < 10; index++)
result += String.fromCharCode(buf[index]);
return result;
};
const bin = typeof(base64) === "string" ? base64 : ab2str10();
if(bin.length < 10) return ImageType.UNKNOWN;
if(bin[0] == String.fromCharCode(66) && bin[1] == String.fromCharCode(77)) {
@ -470,6 +503,15 @@ class IconManager {
IconManager.cache = new CacheManager("icons");
}
async delete_icon(id: number) : Promise<void> {
if(id <= 1000)
throw "invalid id!";
await this.handle.delete_file({
name: '/icon_' + id
});
}
iconList() : Promise<FileEntry[]> {
return this.handle.requestFileList("/icons");
}

View File

@ -1,7 +1,9 @@
enum ErrorID {
PERMISSION_ERROR = 2568,
EMPTY_RESULT = 0x0501,
PLAYLIST_IS_IN_USE = 0x2103
PLAYLIST_IS_IN_USE = 0x2103,
FILE_ALREADY_EXISTS = 2050,
}
class CommandResult {

View File

@ -84,6 +84,11 @@ namespace i18n {
return translated;
}
export function tra(message: string, ...args: any[]) {
message = tr(message);
return MessageHelper.formatMessage(message, ...args);
}
async function load_translation_file(url: string, path: string) : Promise<TranslationFile> {
return new Promise<TranslationFile>((resolve, reject) => {
$.ajax({
@ -296,4 +301,5 @@ namespace i18n {
}
// @ts-ignore
const tr: typeof i18n.tr = i18n.tr;
const tr: typeof i18n.tr = i18n.tr;
const tra: typeof i18n.tra = i18n.tra;

View File

@ -541,6 +541,7 @@ const loader_javascript = {
"js/crypto/sha.js",
"js/crypto/hex.js",
"js/crypto/asn1.js",
"js/crypto/crc32.js",
//load the profiles
"js/profiles/ConnectionProfile.js",

View File

@ -326,6 +326,7 @@ function main() {
password: password,
hashed: password_hashed
} : undefined);
Modals.spawnIconUpload(connection);
} else {
Modals.spawnConnectModal({
url: address,
@ -336,6 +337,7 @@ function main() {
});
}
}
}
const task_teaweb_starter: loader.Task = {

View File

@ -85,6 +85,7 @@ class Modal {
shown: boolean;
close_listener: (() => any)[] = [];
close_elements: JQuery;
constructor(props: ModalProperties) {
this.properties = props;
@ -117,7 +118,8 @@ class Modal {
Object.assign(properties, this.properties.template_properties);
const tag = template.renderTag(properties);
this.close_elements = tag.find(".button-modal-close");
this.close_elements.toggle(this.properties.closeable);
this._htmlTag = tag;
this._htmlTag.on('hide.bs.modal', event => !this.properties.closeable || this.close());
this._htmlTag.on('hidden.bs.modal', event => this._htmlTag.detach());
@ -145,6 +147,14 @@ class Modal {
for(const listener of this.close_listener)
listener();
}
set_closeable(flag: boolean) {
if(flag === this.properties.closeable)
return;
this.properties.closeable = flag;
this.close_elements.toggle(flag);
}
}
function createModal(data: ModalProperties | any) : Modal {

View File

@ -6,13 +6,17 @@ namespace Modals {
//TODO Upload/delete button
export function spawnIconSelect(client: ConnectionHandler, callback_icon?: (id: number) => any, selected_icon?: number) {
selected_icon = selected_icon || 0;
let allow_manage = client.permissions.neededPermission(PermissionType.B_ICON_MANAGE).granted(1);
const modal = createModal({
header: tr("Icons"),
footer: undefined,
body: () => {
const template = $("#tmpl_icon_select").renderTag({
enable_select: !!callback_icon
enable_select: !!callback_icon,
enable_upload: allow_manage,
enable_delete: allow_manage
});
return template;
@ -20,6 +24,8 @@ namespace Modals {
});
const button_select = modal.htmlTag.find(".button-select");
const button_delete = modal.htmlTag.find(".button-delete").prop("disabled", true);
const button_upload = modal.htmlTag.find(".button-upload").prop("disabled", !allow_manage);
const container_loading = modal.htmlTag.find(".container-loading").hide();
const container_no_permissions = modal.htmlTag.find(".container-no-permissions").hide();
@ -90,7 +96,7 @@ namespace Modals {
continue;
}
const tag = client.fileManager.icons.generateTag(icon_id, {animate: false}).attr('title', "Icon " + icon_id);
if(callback_icon) {
if(callback_icon || allow_manage) {
tag.on('click', event => {
container_icons.find(".selected").removeClass("selected");
tag.addClass("selected");
@ -98,8 +104,12 @@ namespace Modals {
selected_container.empty().append(tag.clone());
selected_icon = icon_id;
button_select.prop("disabled", false);
button_delete.prop("disabled", !allow_manage);
});
tag.on('dblclick', event => {
if(!callback_icon)
return;
callback_icon(icon_id);
modal.close();
});
@ -127,6 +137,31 @@ namespace Modals {
});
};
button_delete.on('click', event => {
if(!selected_icon)
return;
const selected = modal.htmlTag.find(".selected");
if(selected.length != 1)
console.warn(tr("UI selected icon length does not equal with 1! (%o)"), selected.length);
if(selected_icon < 1000) return; /* we cant delete local icons */
client.fileManager.icons.delete_icon(selected_icon).then(() => {
selected.detach();
}).catch(error => {
if(error instanceof CommandResult && error.id == ErrorID.PERMISSION_ERROR)
return;
console.warn(tr("Failed to delete icon %d: %o"), selected_icon, error);
error = error instanceof CommandResult ? error.extra_message || error.message : error;
createErrorModal(tr("Failed to delete icon"), tra("Failed to delete icon.<br>Error: ", error)).open();
});
});
button_upload.on('click', event => spawnIconUpload(client));
update_local_icons([100, 200, 300, 500, 600]);
update_remote_icons();
modal.htmlTag.find('.button-reload').on('click', () => update_remote_icons());
@ -140,4 +175,465 @@ namespace Modals {
});
modal.open();
}
interface UploadingIcon {
file: File;
state: "loading" | "valid" | "error";
upload_state: "unset" | "uploading" | "uploaded" | "error";
html_tag?: JQuery;
image_element?: () => HTMLImageElement;
loader: Promise<void>;
upload_icon: () => () => Promise<void>;
upload_html_tag?: JQuery;
icon_id: string;
}
function handle_icon_upload(file: File, client: ConnectionHandler) : UploadingIcon {
const icon = {} as UploadingIcon;
icon.file = file;
icon.upload_state = "unset";
const file_too_big = () => {
console.error(tr("Failed to load file %s: File is too big!"), file.name);
createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.<br>The given file is too big!", file.name)).open();
icon.state = "error";
};
if(file.size > 1024 * 1024 * 512) {
file_too_big();
} else if((file.size | 0) <= 0) {
console.error(tr("Failed to load file %s: Your browser does not support file sizes!"), file.name);
createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.<br>Your browser does not support file sizes!", file.name)).open();
icon.state = "error";
return;
} else {
icon.state = "loading";
icon.loader = (async () => {
const reader = new FileReader();
try {
await new Promise((resolve, reject) => {
reader.onload = resolve;
reader.onerror = reject;
reader.readAsDataURL(file);
});
} catch(error) {
console.log("Image failed to load (%o)", error);
console.error(tr("Failed to load file %s: Image failed to load"), file.name);
createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.<br>Failed to load image", file.name)).open();
icon.state = "error";
return;
}
const result = reader.result as string;
if(typeof(result) !== "string") {
console.error(tr("Failed to load file %s: Result is not an media string (%o)"), file.name, result);
createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.<br>Result is not an media string", file.name)).open();
icon.state = "error";
return;
}
/* get the CRC32 sum */
{
if(!result.startsWith("data:image/")) {
console.error(tr("Failed to load file %s: Invalid data media type (%o)"), file.name, result);
createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.<br>File is not an image", file.name)).open();
icon.state = "error";
return;
}
const semi = result.indexOf(';');
const type = result.substring(11, semi);
console.log(tr("Given image has type %s"), type);
if(!result.substr(semi + 1).startsWith("base64,")) {
console.error(tr("Failed to load file %s: Mimetype isnt base64 encoded (%o)"), file.name, result.substr(semi + 1));
createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.<br>Decoder returned unknown result", file.name)).open();
icon.state = "error";
return;
}
const crc = new Crc32();
crc.update(arrayBufferBase64(result.substr(semi + 8)));
icon.icon_id = crc.digest(10);
}
const image = document.createElement("img");
try {
await new Promise((resolve, reject) => {
image.onload = resolve;
image.onerror = reject;
image.src = result;
});
} catch(error) {
console.log("Image failed to load (%o)", error);
console.error(tr("Failed to load file %s: Image failed to load"), file.name);
createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.<br>Failed to load image", file.name)).open();
icon.state = "error";
}
const width_error = message => {
console.error(tr("Failed to load file %s: Invalid bounds: %s"), file.name, message);
createErrorModal(tr("Icon upload failed"), tra("Failed to upload icon {}.<br>Image is too large ({})", file.name, message)).open();
icon.state = "error";
};
if(image.naturalWidth > 32 && image.naturalHeight > 32) {
width_error("width and height (max 32px)");
return;
}
if(image.naturalWidth > 32) {
width_error("width (max 32px)");
return;
}
if(image.naturalHeight > 32) {
width_error("height (max 32px)");
return;
}
console.log("Image loaded (%dx%d) %s (%s)", image.naturalWidth, image.naturalHeight, image.name, icon.icon_id);
icon.image_element = () => {
const image = document.createElement("img");
image.src = result;
return image;
};
icon.state = "valid";
})();
icon.upload_icon = () => {
const create_progress_bar = () => {
const html = $.spawn("div").addClass("progress");
const indicator = $.spawn("div").addClass("progress-bar bg-success progress-bar-striped progress-bar-animated");
const message = $.spawn("div").addClass("progress-message");
const set_value = value => {
indicator.stop(true, false).animate({width: value + "%"}, 250);
if(value === 100)
setTimeout(() => indicator.removeClass("progress-bar-striped progress-bar-animated"), 900)
};
return {
html_tag: html.append(indicator).append(message),
set_value: set_value,
set_message: msg => message.text(msg),
set_error: msg => {
message.text(tr("error: ") + msg);
set_value(100);
indicator.removeClass("bg-success").addClass("bg-danger");
}
}
};
const container_image = $.spawn("div").addClass("container-icon");
const bar = create_progress_bar();
const set_error = message => {
bar.set_value(100);
bar.set_message(tr("error: ") + message);
};
const html_tag = $.spawn("div")
.addClass("upload-entry")
.append(container_image)
.append(bar.html_tag);
icon.upload_html_tag = html_tag;
let icon_added = false;
if(icon.image_element) {
container_image.append(icon.image_element());
icon_added = true;
}
bar.set_value(0);
bar.set_value(tr("waiting"));
return async () => {
const time_begin = Date.now();
if(icon.state === "loading") {
bar.set_message(tr("Awaiting local processing"));
await icon.loader;
// @ts-ignore Could happen because the loader function updates the state
if(icon.state !== "valid") {
set_error(tr("local processing failed"));
icon.upload_state = "error";
return;
}
} else if(icon.state === "error") {
set_error(tr("local processing error"));
icon.upload_state = "error";
return;
}
if(!icon_added)
container_image.append(icon.image_element());
bar.set_value(25);
bar.set_message(tr("initializing"));
let upload_key: transfer.UploadKey;
try {
upload_key = await client.fileManager.upload_file({
channel: undefined,
channel_password: undefined,
name: '/icon_' + icon.icon_id,
overwrite: false,
path: '',
size: icon.file.size
})
} catch(error) {
if(error instanceof CommandResult && error.id == ErrorID.FILE_ALREADY_EXISTS) {
await new Promise(resolve => setTimeout(resolve, 500 + Math.floor(Math.random() * 500)));
bar.set_message(tr("icon already exists"));
bar.set_value(100);
icon.upload_state = "uploaded";
return;
}
console.error(tr("Failed to initialize upload: %o"), error);
bar.set_error(tr("failed to initialize upload"));
icon.upload_state = "error";
return;
}
bar.set_value(50);
bar.set_message(tr("uploading"));
const connection = transfer.spawn_upload_transfer(upload_key);
try {
await connection.put_data(icon.file)
} catch(error) {
console.error(tr("Icon upload failed for icon %s: %o"), icon.file.name, error);
if(typeof(error) === "string")
bar.set_error(tr("upload failed: ") + error);
else
bar.set_error(tr("upload failed"));
icon.upload_state = "error";
return;
}
const time_end = Date.now();
await new Promise(resolve => setTimeout(resolve, Math.max(0, 1000 - (time_end - time_begin))));
bar.set_value(100);
bar.set_message(tr("upload completed"));
icon.upload_state = "uploaded";
};
};
}
return icon;
}
export function spawnIconUpload(client: ConnectionHandler) {
const modal = createModal({
header: tr("Upload Icons"),
footer: undefined,
body: () => {
const template = $("#tmpl_icon_upload").renderTag();
return template;
},
closeable: false
});
const button_upload = modal.htmlTag.find(".button-upload");
const button_delete = modal.htmlTag.find(".button-remove").prop("disabled", true);
const button_add = modal.htmlTag.find(".button-add");
const button_upload_abort = modal.htmlTag.find(".button-upload-abort");
const input_file = modal.htmlTag.find(".input-file-upload") as JQuery<HTMLInputElement>;
const container_icons = modal.htmlTag.find(".container-icons");
let selected_icon: UploadingIcon;
let icons: UploadingIcon[] = [];
const update_upload_button = () => {
const icon_count = icons.filter(e => e.state === "valid").length;
button_upload.empty();
tra("Upload icons ({})", icon_count).forEach(e => e.appendTo(button_upload));
button_upload.prop("disabled", icon_count == 0);
};
update_upload_button();
const add_icon = (icon: UploadingIcon) => {
icons.push(icon);
icon.loader.then(e => {
if(icon.state === "valid") {
const image = icon.image_element();
const element = $.spawn("div")
.addClass("icon-container")
.append(image);
container_icons.append(icon.html_tag = element);
element.on('click', event => {
container_icons.find(".selected").removeClass("selected");
element.addClass("selected");
selected_icon = icon;
button_delete.prop("disabled", false);
});
update_upload_button();
}
});
};
button_delete.on('click', event => {
if(!selected_icon)
return;
icons = icons.filter(e => e !== selected_icon);
if(selected_icon.html_tag)
selected_icon.html_tag.detach();
button_delete.prop("disabled", true);
update_upload_button();
});
button_add.on('click', event => input_file.click());
input_file.on('change', event => {
if(input_file[0].files.length > 0) {
for(let index = 0; index < input_file[0].files.length; index++) {
const file = input_file[0].files.item(index);
{
let duplicate = false;
for(const icon of icons)
if(icon.file.name === file.name && icon.file.lastModified === file.lastModified && icon.state !== "error") {
duplicate = true;
break;
}
if(duplicate)
continue;
}
add_icon(handle_icon_upload(file, client));
}
}
});
container_icons.on('dragover', ((event: DragEvent) => {
event.stopPropagation();
event.preventDefault();
event.dataTransfer.dropEffect = 'copy';
}) as any);
container_icons.on('drop', ((event: DragEvent) => {
event.stopPropagation();
event.preventDefault();
for(let index = 0; index < event.dataTransfer.files.length; index++) {
const file = event.dataTransfer.files.item(index);
{
let duplicate = false;
for(const icon of icons)
if(icon.file === file && icon.state !== "error") {
duplicate = true;
break;
}
if(duplicate)
continue;
}
add_icon(handle_icon_upload(file, client));
}
}) as any);
/* upload process */
{
const container_upload = modal.htmlTag.find(".container-upload");
const container_error = container_upload.find(".container-error");
const container_success = container_upload.find(".container-success");
const container_process = container_upload.find(".container-process");
const container_info = container_upload.find(".container-info");
const container_statistics = container_upload.find(".uploaded-statistics");
const show_critical_error = message => {
container_error.find(".error-message").text(message);
container_error.show();
};
const finish_upload = () => {
icons = icons.filter(e => {
if(e.upload_state === "uploaded") {
e.html_tag.detach();
return false;
}
return true;
});
update_upload_button();
button_upload.prop("disabled", false);
button_upload.prop("disabled", false);
container_upload.hide();
container_error.hide();
modal.set_closeable(true);
};
const execute_upload = async () => {
if(!client || !client.fileManager) {
show_critical_error(tr("Invalid client handle"));
return;
}
if(!client.connected) {
show_critical_error(tr("Not connected"));
return;
}
let invoke_count = 0;
let succeed_count = 0;
let failed_count = 0;
const uploads = icons.filter(e => e.state !== "error");
const executes: {icon: UploadingIcon, task: () => Promise<void>}[] = [];
for(const icon of uploads) {
executes.push({
icon: icon,
task: icon.upload_icon()
});
if(!icon.upload_html_tag)
continue; /* TODO: error? */
icon.upload_html_tag.appendTo(container_process);
}
const update_state = () => container_statistics.text(invoke_count + " | " + succeed_count + " | " + failed_count);
for(const execute of executes) {
invoke_count++;
update_state();
try {
await execute.task();
if(execute.icon.upload_state !== "uploaded")
throw "failed";
succeed_count++;
} catch(error) {
failed_count++;
}
update_state();
}
container_info.css({opacity: 1}).animate({opacity: 0}, 250, () => container_info.css({opacity: undefined}).hide());
container_success.find(".message").html(
"Total icons: " + invoke_count + "<br>" +
"Succeeded icons: " + succeed_count + "<br>" +
"Failed icons: " + failed_count
);
container_success.css({opacity: 0}).show().animate({opacity: 1}, 250, () => container_success.css({opacity: undefined}));
};
button_upload.on('click', event => {
modal.set_closeable(false);
button_upload.prop("disabled", true);
button_delete.prop("disabled", true);
button_add.prop("disabled", true);
container_process.empty();
container_upload.show();
execute_upload();
});
button_upload_abort.on('click', event => finish_upload());
container_success.hide();
container_upload.hide();
container_error.hide();
}
modal.open();
modal.set_closeable(true);
}
}

View File

@ -79,7 +79,11 @@ class RecorderProfile {
async initialize() : Promise<void> {
await this.load();
await this.reinitialize_filter();
await this.input.start();
try {
await this.input.start();
} catch(error) {
console.warn(tr("Failed to start recorder after initialize (%o)"), error);
}
}
private initialize_input() {
@ -96,6 +100,7 @@ class RecorderProfile {
this.callback_stop();
};
/*
this.input.callback_state_change = () => {
const new_state = this.input.current_state() === audio.recorder.InputState.RECORDING || this.input.current_state() === audio.recorder.InputState.DRY;
@ -106,6 +111,7 @@ class RecorderProfile {
if(this.callback_support_change)
this.callback_support_change();
}
*/
}
private async load() {