1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
|
use leptos::prelude::*;
use profile_settings::ProfileSettings;
use crate::{components::{icon::IconComponent, modal::Modal}, icon::Icon};
mod profile_settings {
use filamento::error::{AvatarPublishError, CommandError};
use thiserror::Error;
use leptos::prelude::*;
use web_sys::{js_sys::Uint8Array, wasm_bindgen::{prelude::Closure, JsCast, UnwrapThrowExt}, Event, FileReader, HtmlInputElement, ProgressEvent};
use crate::{client::Client, files::Files};
#[derive(Debug, Clone, Error)]
pub enum ProfileSaveError {
#[error("avatar publish: {0}")]
Avatar(#[from] CommandError<AvatarPublishError<Files>>),
}
#[component]
pub fn ProfileSettings() -> impl IntoView {
let client: Client = use_context().expect("no client in context");
// TODO: compartmentalise into error component, form component...
let (error, set_error) = signal(None::<ProfileSaveError>);
let error_message = move || {
error.with(|error| {
if let Some(error) = error {
view! { <div class="error">{error.to_string()}</div> }.into_any()
} else {
view! {}.into_any()
}
})
};
let (profile_save_pending, set_profile_save_pending) = signal(false);
let profile_upload_data = RwSignal::new(None::<Vec<u8>>);
let from_input = move |ev: Event| {
let elem = ev.target().unwrap().unchecked_into::<HtmlInputElement>();
// let UploadSignal(file_signal) = expect_context();
// file_signal.update(Vec::clear); // Clear list from previous change
let files = elem.files().unwrap_throw();
if let Some(file) = files.get(0) {
let reader = FileReader::new().unwrap_throw();
// let name = file.name();
// This closure only needs to be called a single time as we will just
// remake it on each loop
// * web_sys drops it for us when using this specific constructor
let read_file = {
// FileReader is cloned prior to moving into the closure
let reader = reader.to_owned();
Closure::once_into_js(move |_: ProgressEvent| {
// `.result` valid after the `read_*` completes on FileReader
// https://developer.mozilla.org/en-US/docs/Web/API/FileReader/result
let result = reader.result().unwrap_throw();
let data= Uint8Array::new(&result).to_vec();
// Do whatever you want with the Vec<u8>
profile_upload_data.set(Some(data));
})
};
reader.set_onloadend(Some(read_file.as_ref().unchecked_ref()));
// read_as_array_buffer takes a &Blob
//
// Per https://w3c.github.io/FileAPI/#file-section
// > A File object is a Blob object with a name attribute..
//
// File is a subclass (inherits) from the Blob interface, so a File
// can be used anywhere a Blob is required.
reader.read_as_array_buffer(&file).unwrap_throw();
// You can also use `.read_as_text(&file)` instead if you just want a string.
// This example shows how to extract an array buffer as it is more flexible
//
// If you use `.read_as_text` change the closure from ...
//
// let result = reader.result().unwrap_throw();
// let vec_of_u8_bytes = Uint8Array::new(&result).to_vec();
// let content = String::from_utf8(vec_of_u8_bytes).unwrap_throw();
//
// to ...
//
// let result = reader.result().unwrap_throw();
// let content = result.as_string().unwrap_throw();
} else {
profile_upload_data.set(None);
}
};
let save_profile = Action::new_local(move |_| {
let client = client.clone();
async move {}
});
let new_nick= RwSignal::new("".to_string());
view! {
<div class="profile-settings">
<form on:submit=move |ev| {
ev.prevent_default();
save_profile.dispatch(());
}>
{error_message}
<div class="change-avatar">
<input type="file" id="client-user-avatar" on:change=from_input />
</div>
<input disabled=profile_save_pending placeholder="Nickname" type="text" id="client-user-nick" bind:value=new_nick name="client-user-nick" />
<input disabled=profile_save_pending class="button" type="submit" value="Save Changes" />
</form>
</div>
<div class="profile-preview">
<h2>Profile Preview</h2>
<div class="preview">
<img />
<div>nick</div>
</div>
</div>
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SettingsPage {
Account,
Chat,
Profile,
Privacy,
}
#[component]
pub fn Settings() -> impl IntoView {
let show_settings: RwSignal<Option<SettingsPage>> = use_context().unwrap();
view! {
<Modal on_background_click=move |_| { show_settings.set(None); }>
<div class="settings panel">
<div class="header">
<h2>Settings</h2>
<div class="header-icon close">
<IconComponent icon=Icon::Close24 on:click=move |_| show_settings.set(None)/>
</div>
</div>
<div class="settings-main">
<div class="settings-sidebar">
<div class:open=move || *show_settings.read() == Some(SettingsPage::Account) on:click=move |_| show_settings.set(Some(SettingsPage::Account))>Account</div>
<div class:open=move || *show_settings.read() == Some(SettingsPage::Chat) on:click=move |_| show_settings.set(Some(SettingsPage::Chat))>Chat</div>
<div class:open=move || *show_settings.read() == Some(SettingsPage::Privacy) on:click=move |_| show_settings.set(Some(SettingsPage::Privacy))>Privacy</div>
<div class:open=move || *show_settings.read() == Some(SettingsPage::Profile) on:click=move |_| show_settings.set(Some(SettingsPage::Profile))>Profile</div>
</div>
<div class="settings-page">
{move || if let Some(page) = show_settings.get() {
match page {
SettingsPage::Account => view! { <div>"account"</div> }.into_any(),
SettingsPage::Chat => view! { <div>"chat"</div> }.into_any(),
SettingsPage::Profile => view! { <ProfileSettings /> }.into_any(),
SettingsPage::Privacy => view! { <div>"privacy"</div> }.into_any(),
}
} else {
view! {}.into_any()
}}
</div>
</div>
</div>
</Modal>
}
}
|