github.com/soulteary/pocket-bookcase@v0.0.0-20240428065142-0b5a9a0fc98a/internal/view/assets/js/page/setting.js (about) 1 var template = ` 2 <div id="page-setting"> 3 <h1 class="page-header">Settings</h1> 4 <div class="setting-container"> 5 <details open class="setting-group" id="setting-display"> 6 <summary>Display</summary> 7 <label> 8 <input type="checkbox" v-model="appOptions.ShowId" @change="saveSetting"> 9 Show bookmark's ID 10 </label> 11 <label> 12 <input type="checkbox" v-model="appOptions.ListMode" @change="saveSetting"> 13 Display bookmarks as list 14 </label> 15 <label> 16 <input type="checkbox" v-model="appOptions.HideThumbnail" @change="saveSetting"> 17 Hide thumbnail image 18 </label> 19 <label> 20 <input type="checkbox" v-model="appOptions.HideExcerpt" @change="saveSetting"> 21 Hide bookmark's excerpt 22 </label> 23 <label> 24 <input type="checkbox" v-model="appOptions.NightMode" @change="saveSetting"> 25 Use dark theme 26 </label> 27 </details> 28 <details v-if="activeAccount.owner" open class="setting-group" id="setting-bookmarks"> 29 <summary>Bookmarks</summary> 30 <label> 31 <input type="checkbox" v-model="appOptions.KeepMetadata" @change="saveSetting"> 32 Keep bookmark's metadata when updating 33 </label> 34 <label> 35 <input type="checkbox" v-model="appOptions.UseArchive" @change="saveSetting"> 36 Create archive by default 37 </label> 38 <label> 39 <input type="checkbox" v-model="appOptions.CreateEbook" @change="saveSetting"> 40 Create ebook by default 41 </label> 42 <label> 43 <input type="checkbox" v-model="appOptions.MakePublic" @change="saveSetting"> 44 Make archive publicly available by default 45 </label> 46 </details> 47 <details v-if="activeAccount.owner" open class="setting-group" id="setting-accounts"> 48 <summary>Accounts</summary> 49 <ul> 50 <li v-if="accounts.length === 0">No accounts registered</li> 51 <li v-for="(account, idx) in accounts"> 52 <p>{{account.username}} 53 <span v-if="account.owner" class="account-level">(owner)</span> 54 </p> 55 <a title="Change password" @click="showDialogChangePassword(account)"> 56 <i class="fa fas fa-fw fa-key"></i> 57 </a> 58 <a title="Delete account" @click="showDialogDeleteAccount(account, idx)"> 59 <i class="fa fas fa-fw fa-trash-alt"></i> 60 </a> 61 </li> 62 </ul> 63 <div class="setting-group-footer"> 64 <a @click="loadAccounts">Refresh accounts</a> 65 <a v-if="activeAccount.owner" @click="showDialogNewAccount">Add new account</a> 66 </div> 67 </details> 68 </div> 69 <div class="loading-overlay" v-if="loading"><i class="fas fa-fw fa-spin fa-spinner"></i></div> 70 <custom-dialog v-bind="dialog"/> 71 </div>`; 72 73 import customDialog from "../component/dialog.js"; 74 import basePage from "./base.js"; 75 76 export default { 77 template: template, 78 mixins: [basePage], 79 components: { 80 customDialog, 81 }, 82 data() { 83 return { 84 loading: false, 85 accounts: [], 86 }; 87 }, 88 methods: { 89 saveSetting() { 90 let options = { 91 ShowId: this.appOptions.ShowId, 92 ListMode: this.appOptions.ListMode, 93 HideThumbnail: this.appOptions.HideThumbnail, 94 HideExcerpt: this.appOptions.HideExcerpt, 95 NightMode: this.appOptions.NightMode, 96 }; 97 98 if (this.activeAccount.owner) { 99 options = { 100 ...options, 101 KeepMetadata: this.appOptions.KeepMetadata, 102 UseArchive: this.appOptions.UseArchive, 103 CreateEbook: this.appOptions.CreateEbook, 104 MakePublic: this.appOptions.MakePublic, 105 }; 106 } 107 108 this.$emit("setting-changed", options); 109 //request 110 fetch(new URL("api/v1/auth/account", document.baseURI), { 111 method: "PATCH", 112 body: JSON.stringify({ 113 config: this.appOptions, 114 }), 115 headers: { 116 "Content-Type": "application/json", 117 Authorization: "Bearer " + localStorage.getItem("shiori-token"), 118 }, 119 }) 120 .then((response) => { 121 if (!response.ok) throw response; 122 return response.json(); 123 }) 124 .then((responseData) => { 125 const responseString = JSON.stringify(responseData.message); 126 localStorage.setItem("shiori-account", responseString); 127 }) 128 .catch((err) => { 129 this.getErrorMessage(err).then((msg) => { 130 this.showErrorDialog(msg); 131 }); 132 }); 133 }, 134 loadAccounts() { 135 if (this.loading) return; 136 137 this.loading = true; 138 fetch(new URL("api/accounts", document.baseURI), { 139 headers: { 140 "Content-Type": "application/json", 141 Authorization: "Bearer " + localStorage.getItem("shiori-token"), 142 }, 143 }) 144 .then((response) => { 145 if (!response.ok) throw response; 146 return response.json(); 147 }) 148 .then((json) => { 149 this.loading = false; 150 this.accounts = json; 151 }) 152 .catch((err) => { 153 this.loading = false; 154 this.getErrorMessage(err).then((msg) => { 155 this.showErrorDialog(msg); 156 }); 157 }); 158 }, 159 showDialogNewAccount() { 160 this.showDialog({ 161 title: "New Account", 162 content: "Input new account's data :", 163 fields: [ 164 { 165 name: "username", 166 label: "Username", 167 value: "", 168 }, 169 { 170 name: "password", 171 label: "Password", 172 type: "password", 173 value: "", 174 }, 175 { 176 name: "repeat", 177 label: "Repeat password", 178 type: "password", 179 value: "", 180 }, 181 { 182 name: "visitor", 183 label: "This account is for visitor", 184 type: "check", 185 value: false, 186 }, 187 ], 188 mainText: "OK", 189 secondText: "Cancel", 190 mainClick: (data) => { 191 if (data.username === "") { 192 this.showErrorDialog("Username must not empty"); 193 return; 194 } 195 196 if (data.password === "") { 197 this.showErrorDialog("Password must not empty"); 198 return; 199 } 200 201 if (data.password !== data.repeat) { 202 this.showErrorDialog("Password does not match"); 203 return; 204 } 205 206 var request = { 207 username: data.username, 208 password: data.password, 209 owner: !data.visitor, 210 }; 211 212 this.dialog.loading = true; 213 fetch(new URL("api/accounts", document.baseURI), { 214 method: "post", 215 body: JSON.stringify(request), 216 headers: { 217 "Content-Type": "application/json", 218 Authorization: "Bearer " + localStorage.getItem("shiori-token"), 219 }, 220 }) 221 .then((response) => { 222 if (!response.ok) throw response; 223 return response; 224 }) 225 .then(() => { 226 this.dialog.loading = false; 227 this.dialog.visible = false; 228 229 this.accounts.push({ 230 username: data.username, 231 owner: !data.visitor, 232 }); 233 this.accounts.sort((a, b) => { 234 var nameA = a.username.toLowerCase(), 235 nameB = b.username.toLowerCase(); 236 237 if (nameA < nameB) { 238 return -1; 239 } 240 241 if (nameA > nameB) { 242 return 1; 243 } 244 245 return 0; 246 }); 247 }) 248 .catch((err) => { 249 this.dialog.loading = false; 250 this.getErrorMessage(err).then((msg) => { 251 this.showErrorDialog(msg); 252 }); 253 }); 254 }, 255 }); 256 }, 257 showDialogChangePassword(account) { 258 this.showDialog({ 259 title: "Change Password", 260 content: "Input new password :", 261 fields: [ 262 { 263 name: "oldPassword", 264 label: "Old password", 265 type: "password", 266 value: "", 267 }, 268 { 269 name: "password", 270 label: "New password", 271 type: "password", 272 value: "", 273 }, 274 { 275 name: "repeat", 276 label: "Repeat password", 277 type: "password", 278 value: "", 279 }, 280 ], 281 mainText: "OK", 282 secondText: "Cancel", 283 mainClick: (data) => { 284 if (data.oldPassword === "") { 285 this.showErrorDialog("Old password must not empty"); 286 return; 287 } 288 289 if (data.password === "") { 290 this.showErrorDialog("New password must not empty"); 291 return; 292 } 293 294 if (data.password !== data.repeat) { 295 this.showErrorDialog("Password does not match"); 296 return; 297 } 298 299 var request = { 300 username: account.username, 301 oldPassword: data.oldPassword, 302 newPassword: data.password, 303 owner: account.owner, 304 }; 305 306 this.dialog.loading = true; 307 fetch(new URL("api/accounts", document.baseURI), { 308 method: "put", 309 body: JSON.stringify(request), 310 headers: { 311 "Content-Type": "application/json", 312 Authorization: "Bearer " + localStorage.getItem("shiori-token"), 313 }, 314 }) 315 .then((response) => { 316 if (!response.ok) throw response; 317 return response; 318 }) 319 .then(() => { 320 this.dialog.loading = false; 321 this.dialog.visible = false; 322 }) 323 .catch((err) => { 324 this.dialog.loading = false; 325 this.getErrorMessage(err).then((msg) => { 326 this.showErrorDialog(msg); 327 }); 328 }); 329 }, 330 }); 331 }, 332 showDialogDeleteAccount(account, idx) { 333 this.showDialog({ 334 title: "Delete Account", 335 content: `Delete account "${account.username}" ?`, 336 mainText: "Yes", 337 secondText: "No", 338 mainClick: () => { 339 this.dialog.loading = true; 340 fetch(`api/accounts`, { 341 method: "delete", 342 body: JSON.stringify([account.username]), 343 headers: { 344 "Content-Type": "application/json", 345 Authorization: "Bearer " + localStorage.getItem("shiori-token"), 346 }, 347 }) 348 .then((response) => { 349 if (!response.ok) throw response; 350 return response; 351 }) 352 .then(() => { 353 this.dialog.loading = false; 354 this.dialog.visible = false; 355 this.accounts.splice(idx, 1); 356 }) 357 .catch((err) => { 358 this.dialog.loading = false; 359 this.getErrorMessage(err).then((msg) => { 360 this.showErrorDialog(msg); 361 }); 362 }); 363 }, 364 }); 365 }, 366 }, 367 mounted() { 368 this.loadAccounts(); 369 }, 370 };