account.html 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  6. <title>User Account</title>
  7. <script src="../../script/jquery.min.js"></script>
  8. <script src="../../script/ao_module.js"></script>
  9. <script src="../../script/applocale.js"></script>
  10. <style>
  11. * { box-sizing: border-box; margin: 0; padding: 0; }
  12. body { background: transparent; overflow-x: hidden; }
  13. #ua-root {
  14. --ua-bg: #f3f3f3;
  15. --ua-card: #ffffff;
  16. --ua-border: #e0e0e0;
  17. --ua-text: #1b1b1b;
  18. --ua-dim: #5a5a5a;
  19. --ua-muted: #8a8a8a;
  20. --ua-accent: #0067c0;
  21. --ua-accentH: #1475c8;
  22. --ua-danger: #c42b1c;
  23. --ua-shadow: 0 1px 4px rgba(0,0,0,0.08);
  24. --ua-radius: 6px;
  25. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  26. font-size: 14px;
  27. color: var(--ua-text);
  28. background: var(--ua-bg);
  29. min-height: 100vh;
  30. padding-bottom: 32px;
  31. }
  32. #ua-root.dark {
  33. --ua-bg: #202020;
  34. --ua-card: #2d2d2d;
  35. --ua-border: #404040;
  36. --ua-text: #e8e8e8;
  37. --ua-dim: #aaaaaa;
  38. --ua-muted: #666666;
  39. --ua-accent: #60cdff;
  40. --ua-accentH: #8cd9ff;
  41. --ua-danger: #ff7070;
  42. --ua-shadow: 0 1px 6px rgba(0,0,0,0.4);
  43. }
  44. /* -- Hero bar ---------------------------------------------------------------- */
  45. #ua-hero {
  46. display: flex;
  47. align-items: center;
  48. gap: 16px;
  49. padding: 22px 20px 18px;
  50. }
  51. #ua-hero-avatar {
  52. width: 72px;
  53. height: 72px;
  54. border-radius: 50%;
  55. overflow: hidden;
  56. flex-shrink: 0;
  57. border: 2px solid var(--ua-border);
  58. background: var(--ua-border);
  59. }
  60. #ua-hero-avatar img {
  61. width: 100%;
  62. height: 100%;
  63. object-fit: cover;
  64. display: block;
  65. }
  66. #ua-hero-info { flex: 1; min-width: 0; }
  67. #ua-hero-name {
  68. font-size: 18px;
  69. font-weight: 600;
  70. color: var(--ua-text);
  71. margin-bottom: 4px;
  72. overflow: hidden;
  73. text-overflow: ellipsis;
  74. white-space: nowrap;
  75. }
  76. #ua-hero-group {
  77. font-size: 13px;
  78. color: var(--ua-dim);
  79. }
  80. /* -- Section cards ----------------------------------------------------------- */
  81. .ua-section {
  82. background: var(--ua-card);
  83. border: 1px solid var(--ua-border);
  84. border-radius: var(--ua-radius);
  85. margin: 0 12px 10px;
  86. box-shadow: var(--ua-shadow);
  87. overflow: hidden;
  88. }
  89. .ua-section-header {
  90. display: flex;
  91. align-items: center;
  92. gap: 10px;
  93. padding: 14px 18px;
  94. cursor: pointer;
  95. user-select: none;
  96. transition: background 0.1s;
  97. }
  98. .ua-section-header:hover { background: rgba(0,0,0,0.035); }
  99. #ua-root.dark .ua-section-header:hover { background: rgba(255,255,255,0.05); }
  100. .ua-section-icon { color: var(--ua-dim); flex-shrink: 0; }
  101. .ua-section-title {
  102. flex: 1;
  103. font-size: 14px;
  104. font-weight: 600;
  105. color: var(--ua-text);
  106. }
  107. .ua-chevron {
  108. width: 14px;
  109. height: 14px;
  110. color: var(--ua-muted);
  111. transition: transform 0.2s ease;
  112. flex-shrink: 0;
  113. }
  114. .ua-chevron.open { transform: rotate(180deg); }
  115. .ua-section-body {
  116. display: none;
  117. padding: 4px 18px 18px;
  118. border-top: 1px solid var(--ua-border);
  119. }
  120. .ua-section-body.open { display: block; }
  121. /* -- Form elements ----------------------------------------------------------- */
  122. .ua-label {
  123. display: block;
  124. font-size: 12px;
  125. font-weight: 500;
  126. color: var(--ua-dim);
  127. margin-bottom: 5px;
  128. margin-top: 14px;
  129. }
  130. .ua-input {
  131. width: 100%;
  132. padding: 7px 10px;
  133. border: 1px solid var(--ua-border);
  134. border-radius: 4px;
  135. background: var(--ua-card);
  136. color: var(--ua-text);
  137. font-family: inherit;
  138. font-size: 13px;
  139. outline: none;
  140. transition: border-color 0.15s;
  141. }
  142. .ua-input:focus { border-color: var(--ua-accent); }
  143. .ua-input.error { border-color: var(--ua-danger) !important; }
  144. .ua-btn {
  145. display: inline-flex;
  146. align-items: center;
  147. gap: 6px;
  148. padding: 7px 16px;
  149. border-radius: 4px;
  150. border: 1px solid transparent;
  151. font-family: inherit;
  152. font-size: 13px;
  153. font-weight: 500;
  154. cursor: pointer;
  155. transition: background 0.1s;
  156. outline: none;
  157. }
  158. .ua-btn-primary {
  159. background: var(--ua-accent);
  160. color: #fff;
  161. border-color: var(--ua-accent);
  162. }
  163. .ua-btn-primary:hover { background: var(--ua-accentH); border-color: var(--ua-accentH); }
  164. .ua-btn-secondary {
  165. background: transparent;
  166. color: var(--ua-text);
  167. border-color: var(--ua-border);
  168. }
  169. .ua-btn-secondary:hover { background: rgba(0,0,0,0.05); }
  170. #ua-root.dark .ua-btn-secondary:hover { background: rgba(255,255,255,0.07); }
  171. .ua-actions { margin-top: 16px; display: flex; gap: 8px; flex-wrap: wrap; }
  172. /* -- Profile picture section ------------------------------------------------- */
  173. #pic-preview-row {
  174. display: flex;
  175. align-items: center;
  176. gap: 16px;
  177. margin-top: 14px;
  178. margin-bottom: 4px;
  179. }
  180. #pic-preview {
  181. width: 72px;
  182. height: 72px;
  183. border-radius: 50%;
  184. overflow: hidden;
  185. border: 2px solid var(--ua-border);
  186. flex-shrink: 0;
  187. background: var(--ua-border);
  188. }
  189. #pic-preview img {
  190. width: 100%;
  191. height: 100%;
  192. object-fit: cover;
  193. display: block;
  194. }
  195. .pic-btn-col { display: flex; flex-direction: column; gap: 8px; }
  196. /* -- Crop modal -------------------------------------------------------------- */
  197. #ua-crop-overlay {
  198. display: none;
  199. position: fixed;
  200. inset: 0;
  201. z-index: 9000;
  202. background: rgba(0,0,0,0.6);
  203. align-items: center;
  204. justify-content: center;
  205. }
  206. #ua-crop-overlay.open { display: flex; }
  207. #ua-crop-dialog {
  208. background: #fff;
  209. border-radius: 10px;
  210. padding: 22px;
  211. width: 340px;
  212. max-width: 95vw;
  213. box-shadow: 0 12px 50px rgba(0,0,0,0.4);
  214. }
  215. #ua-crop-overlay.dark #ua-crop-dialog { background: #2d2d2d; }
  216. #ua-crop-dialog-title {
  217. font-size: 15px;
  218. font-weight: 600;
  219. color: #1b1b1b;
  220. margin-bottom: 14px;
  221. }
  222. #ua-crop-overlay.dark #ua-crop-dialog-title { color: #e8e8e8; }
  223. #ua-crop-stage {
  224. width: 280px;
  225. height: 280px;
  226. margin: 0 auto 14px;
  227. border-radius: 50%;
  228. overflow: hidden;
  229. position: relative;
  230. border: 2px solid #0067c0;
  231. cursor: grab;
  232. user-select: none;
  233. background: #111;
  234. touch-action: none;
  235. }
  236. #ua-crop-overlay.dark #ua-crop-stage { border-color: #60cdff; }
  237. #ua-crop-stage:active { cursor: grabbing; }
  238. #ua-crop-img {
  239. position: absolute;
  240. pointer-events: none;
  241. transform-origin: top left;
  242. }
  243. #ua-zoom-row {
  244. display: flex;
  245. align-items: center;
  246. gap: 10px;
  247. margin-bottom: 14px;
  248. }
  249. .ua-zoom-label {
  250. font-size: 12px;
  251. color: #5a5a5a;
  252. white-space: nowrap;
  253. flex-shrink: 0;
  254. }
  255. #ua-crop-overlay.dark .ua-zoom-label { color: #aaa; }
  256. #ua-crop-zoom {
  257. flex: 1;
  258. accent-color: #0067c0;
  259. }
  260. #ua-crop-overlay.dark #ua-crop-zoom { accent-color: #60cdff; }
  261. .ua-crop-actions { display: flex; gap: 8px; justify-content: flex-end; }
  262. .ua-crop-btn {
  263. display: inline-flex;
  264. align-items: center;
  265. padding: 7px 16px;
  266. border-radius: 4px;
  267. border: 1px solid transparent;
  268. font-family: inherit;
  269. font-size: 13px;
  270. font-weight: 500;
  271. cursor: pointer;
  272. outline: none;
  273. }
  274. .ua-crop-btn-cancel {
  275. background: transparent;
  276. color: #1b1b1b;
  277. border-color: #e0e0e0;
  278. }
  279. #ua-crop-overlay.dark .ua-crop-btn-cancel { color: #e8e8e8; border-color: #404040; }
  280. .ua-crop-btn-apply {
  281. background: #0067c0;
  282. color: #fff;
  283. border-color: #0067c0;
  284. }
  285. #ua-crop-overlay.dark .ua-crop-btn-apply { background: #60cdff; border-color: #60cdff; color: #1b1b1b; }
  286. /* -- Toast ------------------------------------------------------------------- */
  287. #ua-toast {
  288. display: none;
  289. position: fixed;
  290. left: 50%;
  291. bottom: 18px;
  292. transform: translateX(-50%);
  293. z-index: 9100;
  294. background: #1f1f1f;
  295. border: none;
  296. border-radius: 12px;
  297. padding: 10px 14px;
  298. box-shadow: 0 8px 24px rgba(0,0,0,0.22);
  299. max-width: min(340px, calc(100vw - 24px));
  300. min-width: 180px;
  301. text-align: center;
  302. }
  303. #ua-toast.dark { background: #2d2d2d; }
  304. #ua-toast.error { background: #c42b1c; }
  305. #ua-toast.dark.error { background: #ff7070; }
  306. #ua-toast-title {
  307. font-weight: 600;
  308. font-size: 13px;
  309. color: #ffffff;
  310. margin-bottom: 2px;
  311. }
  312. #ua-toast.dark #ua-toast-title { color: #e8e8e8; }
  313. #ua-toast.error #ua-toast-title,
  314. #ua-toast.dark.error #ua-toast-title { color: #ffffff; }
  315. #ua-toast-msg {
  316. font-size: 12px;
  317. color: rgba(255,255,255,0.9);
  318. }
  319. #ua-toast.dark #ua-toast-msg { color: #aaa; }
  320. #ua-toast.error #ua-toast-msg,
  321. #ua-toast.dark.error #ua-toast-msg { color: rgba(255,255,255,0.94); }
  322. /* -- Misc -------------------------------------------------------------------- */
  323. #ua-current-email {
  324. font-size: 13px;
  325. color: var(--ua-dim);
  326. margin-top: 12px;
  327. margin-bottom: 2px;
  328. }
  329. </style>
  330. </head>
  331. <body>
  332. <!-- Hidden file input -->
  333. <input type="file" id="ua-file-input" accept="image/*" style="display:none">
  334. <!-- Crop modal (outside #ua-root so overlay covers full viewport) -->
  335. <div id="ua-crop-overlay">
  336. <div id="ua-crop-dialog">
  337. <div id="ua-crop-dialog-title" locale="account/crop_title">Crop Profile Picture</div>
  338. <div id="ua-crop-stage">
  339. <img id="ua-crop-img" src="" alt="" draggable="false">
  340. </div>
  341. <div id="ua-zoom-row">
  342. <span class="ua-zoom-label" locale="account/crop_zoom">Zoom</span>
  343. <input type="range" id="ua-crop-zoom" min="50" max="300" value="100" step="1">
  344. </div>
  345. <div class="ua-crop-actions">
  346. <button class="ua-crop-btn ua-crop-btn-cancel" onclick="closeCropModal()" locale="account/cancel">Cancel</button>
  347. <button class="ua-crop-btn ua-crop-btn-apply" onclick="applyCrop()" locale="account/apply">Apply</button>
  348. </div>
  349. </div>
  350. </div>
  351. <!-- Toast notification -->
  352. <div id="ua-toast">
  353. <div id="ua-toast-title"></div>
  354. <div id="ua-toast-msg"></div>
  355. </div>
  356. <!-- Main panel -->
  357. <div id="ua-root">
  358. <!-- Hero: avatar + name + group -->
  359. <div id="ua-hero">
  360. <div id="ua-hero-avatar">
  361. <img id="heroAvatar" src="../users/img/noprofileicon.svg" alt="">
  362. </div>
  363. <div id="ua-hero-info">
  364. <div id="ua-hero-name">&mdash;</div>
  365. <div id="ua-hero-group">&mdash;</div>
  366. </div>
  367. </div>
  368. <!-- Profile Picture -->
  369. <div class="ua-section">
  370. <div class="ua-section-header" onclick="uaToggle(this)">
  371. <svg class="ua-section-icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
  372. <circle cx="8" cy="5.5" r="2.5" stroke="currentColor" stroke-width="1.3"/>
  373. <path d="M2 14c0-3.31 2.686-5.5 6-5.5s6 2.19 6 5.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
  374. </svg>
  375. <span class="ua-section-title" locale="account/pic_title">Profile Picture</span>
  376. <svg class="ua-chevron" viewBox="0 0 16 16" fill="none">
  377. <path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
  378. </svg>
  379. </div>
  380. <div class="ua-section-body">
  381. <div id="pic-preview-row">
  382. <div id="pic-preview">
  383. <img id="picPreview" src="../users/img/noprofileicon.svg" alt="">
  384. </div>
  385. <div class="pic-btn-col">
  386. <button class="ua-btn ua-btn-secondary" onclick="triggerLocalUpload()">
  387. <svg width="13" height="13" viewBox="0 0 16 16" fill="none">
  388. <path d="M8 11V3M5 6l3-3 3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
  389. <path d="M2 13h12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
  390. </svg>
  391. <span locale="account/upload_local">Upload from Device</span>
  392. </button>
  393. <button class="ua-btn ua-btn-secondary" onclick="triggerServerPick()">
  394. <svg width="13" height="13" viewBox="0 0 16 16" fill="none">
  395. <path d="M2 4.5h12M2 8h8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
  396. <rect x="2" y="2" width="12" height="12" rx="1.5" stroke="currentColor" stroke-width="1.3"/>
  397. <circle cx="12" cy="12" r="3" fill="currentColor" fill-opacity=".15" stroke="currentColor" stroke-width="1.1"/>
  398. <path d="M11 12h2M12 11v2" stroke="currentColor" stroke-width="1.1" stroke-linecap="round"/>
  399. </svg>
  400. <span locale="account/upload_server">Pick from Files</span>
  401. </button>
  402. </div>
  403. </div>
  404. </div>
  405. </div>
  406. <!-- Account (Password) -->
  407. <div class="ua-section">
  408. <div class="ua-section-header" onclick="uaToggle(this)">
  409. <svg class="ua-section-icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
  410. <rect x="4" y="7" width="8" height="7" rx="1" stroke="currentColor" stroke-width="1.3"/>
  411. <path d="M5.5 7V5a2.5 2.5 0 015 0v2" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
  412. </svg>
  413. <span class="ua-section-title" locale="account/pw_title">Account</span>
  414. <svg class="ua-chevron" viewBox="0 0 16 16" fill="none">
  415. <path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
  416. </svg>
  417. </div>
  418. <div class="ua-section-body">
  419. <form onsubmit="handlePasswordChange(event)">
  420. <label class="ua-label" locale="account/old_pw">Old Password</label>
  421. <input class="ua-input" id="opw" type="password" placeholder="Old Password">
  422. <label class="ua-label" locale="account/new_pw">New Password</label>
  423. <input class="ua-input" id="npw" type="password" placeholder="New Password">
  424. <label class="ua-label" locale="account/confirm_pw">Confirm New Password</label>
  425. <input class="ua-input" id="cpw" type="password" placeholder="Confirm New Password">
  426. <div class="ua-actions">
  427. <button class="ua-btn ua-btn-primary" type="submit" locale="account/save">Save Changes</button>
  428. </div>
  429. </form>
  430. </div>
  431. </div>
  432. <!-- Email -->
  433. <div class="ua-section">
  434. <div class="ua-section-header" onclick="uaToggle(this)">
  435. <svg class="ua-section-icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
  436. <rect x="2" y="4" width="12" height="9" rx="1.5" stroke="currentColor" stroke-width="1.3"/>
  437. <path d="M2 7l6 3.5L14 7" stroke="currentColor" stroke-width="1.3"/>
  438. </svg>
  439. <span class="ua-section-title" locale="account/email_title">Email</span>
  440. <svg class="ua-chevron" viewBox="0 0 16 16" fill="none">
  441. <path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
  442. </svg>
  443. </div>
  444. <div class="ua-section-body">
  445. <div id="ua-current-email"></div>
  446. <form onsubmit="handleEmailChange(event)">
  447. <label class="ua-label" locale="account/new_email">New Email</label>
  448. <input class="ua-input" id="newemail" type="email" placeholder="New Email">
  449. <div class="ua-actions">
  450. <button class="ua-btn ua-btn-primary" type="submit" locale="account/save">Save Changes</button>
  451. </div>
  452. </form>
  453. </div>
  454. </div>
  455. </div>
  456. <script>
  457. /* ================================================================
  458. Theme
  459. ================================================================ */
  460. var _isDark = false;
  461. var userInfoApplocale = null;
  462. function applyTheme(dark) {
  463. _isDark = dark;
  464. document.getElementById('ua-root').classList.toggle('dark', dark);
  465. document.getElementById('ua-crop-overlay').classList.toggle('dark', dark);
  466. document.getElementById('ua-toast').classList.toggle('dark', dark);
  467. }
  468. try {
  469. if (preferredTheme || parent.preferredTheme){
  470. var theme = preferredTheme || parent.preferredTheme;
  471. var _pt = (theme === 'dark' || theme === 'darkTheme') ? 'dark' : 'white';
  472. if (_pt) {
  473. applyTheme(_pt !== 'white');
  474. }
  475. }else {
  476. ao_module_getSystemThemeColor(function(c) { applyTheme(c !== 'whiteTheme'); });
  477. }
  478. } catch(e) {
  479. ao_module_getSystemThemeColor(function(c) { applyTheme(c !== 'whiteTheme'); });
  480. }
  481. window.detailPageThemeCallback = function(isDark) { applyTheme(isDark); };
  482. /* ================================================================
  483. Applocale + init
  484. ================================================================ */
  485. function initPage() {
  486. loadUserInfo();
  487. loadEmail();
  488. }
  489. $(document).ready(function() {
  490. if (typeof applocale !== 'undefined') {
  491. userInfoApplocale = NewAppLocale();
  492. userInfoApplocale.init('../locale/users/account.json', function() {
  493. userInfoApplocale.translate();
  494. initPage();
  495. });
  496. } else {
  497. initPage();
  498. }
  499. });
  500. /* ================================================================
  501. Section accordion
  502. ================================================================ */
  503. function uaToggle(header) {
  504. var body = header.nextElementSibling;
  505. var chevron = header.querySelector('.ua-chevron');
  506. var isOpen = body.classList.toggle('open');
  507. chevron.classList.toggle('open', isOpen);
  508. }
  509. /* ================================================================
  510. User info
  511. ================================================================ */
  512. function loadUserInfo() {
  513. $.get('../../system/users/userinfo', function(data) {
  514. if (data.error !== undefined) return;
  515. var src = (data.Icondata && data.Icondata !== '')
  516. ? data.Icondata
  517. : '../users/img/noprofileicon.svg';
  518. $('#heroAvatar, #picPreview').attr('src', src);
  519. $('#ua-hero-name').text(data.Username);
  520. var group = (data.Usergroup && data.Usergroup.length)
  521. ? data.Usergroup.join(' / ')
  522. : '';
  523. $('#ua-hero-group').text(group);
  524. });
  525. }
  526. function loadEmail() {
  527. $.get('../../system/register/email', function(data) {
  528. var notSet = (typeof userInfoApplocale !== 'undefined') ? userInfoApplocale.getString('account/email_not_set', 'Not Configured') : 'Not Configured';
  529. var label = (typeof userInfoApplocale !== 'undefined') ? userInfoApplocale.getString('account/current_email_label', 'Current Email:') : 'Current Email:';
  530. $('#ua-current-email').text(
  531. (data && data !== '') ? label + ' ' + data : '( ' + notSet + ' )'
  532. );
  533. });
  534. }
  535. /* ================================================================
  536. Password change
  537. ================================================================ */
  538. function handlePasswordChange(event) {
  539. event.preventDefault();
  540. var oldPw = $('#opw').val();
  541. var newPw = $('#npw').val();
  542. var confPw = $('#cpw').val();
  543. $('#cpw').removeClass('error');
  544. if (newPw !== confPw) {
  545. $('#cpw').addClass('error');
  546. toast('error',
  547. _s('account/pw_mismatch_title', 'Password Mismatch'),
  548. _s('account/pw_mismatch_msg', 'New passwords do not match.')
  549. );
  550. return;
  551. }
  552. $.ajax({
  553. url: '../../system/users/userinfo',
  554. method: 'POST',
  555. data: { opr: 'changepw', oldpw: oldPw, newpw: newPw },
  556. success: function(data) {
  557. if (data.error !== undefined) {
  558. toast('error', _s('account/update_failed', 'Update Failed'), data.error);
  559. } else {
  560. toast('ok',
  561. _s('account/update_ok', 'Updated'),
  562. _s('account/pw_updated', 'Your password has been updated.')
  563. );
  564. $('#opw, #npw, #cpw').val('');
  565. }
  566. }
  567. });
  568. }
  569. /* ================================================================
  570. Email change
  571. ================================================================ */
  572. function handleEmailChange(event) {
  573. event.preventDefault();
  574. var newEmail = $('#newemail').val();
  575. $.ajax({
  576. url: '../../system/register/email',
  577. data: { email: newEmail },
  578. success: function(data) {
  579. if (data.error !== undefined) {
  580. toast('error', _s('account/update_failed', 'Update Failed'), data.error);
  581. } else {
  582. toast('ok',
  583. _s('account/update_ok', 'Updated'),
  584. _s('account/email_updated', 'Your email has been updated.')
  585. );
  586. $('#newemail').val('');
  587. loadEmail();
  588. }
  589. }
  590. });
  591. }
  592. /* ================================================================
  593. Profile picture
  594. ================================================================ */
  595. function triggerLocalUpload() {
  596. document.getElementById('ua-file-input').click();
  597. }
  598. document.getElementById('ua-file-input').addEventListener('change', function() {
  599. var file = this.files[0];
  600. if (!file) return;
  601. this.value = '';
  602. var reader = new FileReader();
  603. reader.onload = function(e) { openCropModal(e.target.result); };
  604. reader.readAsDataURL(file);
  605. });
  606. function triggerServerPick() {
  607. // Use a named callback to support virtual desktop callback routing.
  608. ao_module_openFileSelector(serverFileSelected, 'user:/', 'file', false);
  609. }
  610. function serverFileSelected(files) {
  611. if (!files || !files.length) return;
  612. var vpath = files[0].filepath;
  613. fetch('../../media/?file=' + encodeURIComponent(vpath))
  614. .then(function(r) {
  615. if (!r.ok) throw new Error('fetch failed');
  616. return r.blob();
  617. })
  618. .then(function(blob) {
  619. var reader = new FileReader();
  620. reader.onload = function(e) { openCropModal(e.target.result); };
  621. reader.readAsDataURL(blob);
  622. })
  623. .catch(function() {
  624. toast('error',
  625. _s('account/update_failed', 'Load Failed'),
  626. _s('account/file_load_error', 'Could not load the selected image.')
  627. );
  628. });
  629. }
  630. function uploadProfilePic(dataURL) {
  631. $.ajax({
  632. url: '../../system/users/userinfo',
  633. method: 'POST',
  634. data: { opr: 'changeprofilepic', picdata: dataURL },
  635. success: function(data) {
  636. if (data.error !== undefined) {
  637. toast('error', _s('account/update_failed', 'Update Failed'), data.error);
  638. } else {
  639. $('#heroAvatar, #picPreview').attr('src', dataURL);
  640. toast('ok',
  641. _s('account/update_ok', 'Updated'),
  642. _s('account/pic_updated', 'Profile picture has been updated.')
  643. );
  644. }
  645. }
  646. });
  647. }
  648. /* ================================================================
  649. Crop modal
  650. ================================================================ */
  651. var STAGE = 280;
  652. var crop = { x: 0, y: 0, scale: 1, drag: false, ox: 0, oy: 0, imgW: 0, imgH: 0 };
  653. function openCropModal(dataURL) {
  654. var img = document.getElementById('ua-crop-img');
  655. crop = { x: 0, y: 0, scale: 1, drag: false, ox: 0, oy: 0, imgW: 0, imgH: 0 };
  656. img.onload = function() {
  657. crop.imgW = img.naturalWidth;
  658. crop.imgH = img.naturalHeight;
  659. var fit = STAGE / Math.min(crop.imgW, crop.imgH);
  660. crop.scale = fit;
  661. var zs = document.getElementById('ua-crop-zoom');
  662. zs.min = Math.max(10, Math.round(fit * 100));
  663. zs.max = Math.round(fit * 500);
  664. zs.value = Math.round(fit * 100);
  665. cropCenter();
  666. };
  667. img.src = dataURL;
  668. document.getElementById('ua-crop-overlay').classList.add('open');
  669. }
  670. function closeCropModal() {
  671. document.getElementById('ua-crop-overlay').classList.remove('open');
  672. document.getElementById('ua-crop-img').src = '';
  673. }
  674. function cropCenter() {
  675. crop.x = (STAGE - crop.imgW * crop.scale) / 2;
  676. crop.y = (STAGE - crop.imgH * crop.scale) / 2;
  677. cropRender();
  678. }
  679. function cropRender() {
  680. var img = document.getElementById('ua-crop-img');
  681. img.style.width = (crop.imgW * crop.scale) + 'px';
  682. img.style.height = (crop.imgH * crop.scale) + 'px';
  683. img.style.left = crop.x + 'px';
  684. img.style.top = crop.y + 'px';
  685. }
  686. document.getElementById('ua-crop-zoom').addEventListener('input', function() {
  687. var ns = parseInt(this.value, 10) / 100;
  688. var r = ns / crop.scale;
  689. var cx = STAGE / 2, cy = STAGE / 2;
  690. crop.x = cx - (cx - crop.x) * r;
  691. crop.y = cy - (cy - crop.y) * r;
  692. crop.scale = ns;
  693. cropRender();
  694. });
  695. var stageEl = document.getElementById('ua-crop-stage');
  696. stageEl.addEventListener('mousedown', function(e) {
  697. crop.drag = true;
  698. crop.ox = e.clientX - crop.x;
  699. crop.oy = e.clientY - crop.y;
  700. e.preventDefault();
  701. });
  702. document.addEventListener('mousemove', function(e) {
  703. if (!crop.drag) return;
  704. crop.x = e.clientX - crop.ox;
  705. crop.y = e.clientY - crop.oy;
  706. cropRender();
  707. });
  708. document.addEventListener('mouseup', function() { crop.drag = false; });
  709. stageEl.addEventListener('touchstart', function(e) {
  710. var t = e.touches[0];
  711. crop.drag = true;
  712. crop.ox = t.clientX - crop.x;
  713. crop.oy = t.clientY - crop.y;
  714. e.preventDefault();
  715. }, { passive: false });
  716. document.addEventListener('touchmove', function(e) {
  717. if (!crop.drag) return;
  718. var t = e.touches[0];
  719. crop.x = t.clientX - crop.ox;
  720. crop.y = t.clientY - crop.oy;
  721. cropRender();
  722. }, { passive: false });
  723. document.addEventListener('touchend', function() { crop.drag = false; });
  724. function applyCrop() {
  725. var srcImg = document.getElementById('ua-crop-img');
  726. var canvas = document.createElement('canvas');
  727. canvas.width = STAGE;
  728. canvas.height = STAGE;
  729. var ctx = canvas.getContext('2d');
  730. ctx.drawImage(srcImg,
  731. -crop.x / crop.scale, -crop.y / crop.scale,
  732. STAGE / crop.scale, STAGE / crop.scale,
  733. 0, 0, STAGE, STAGE
  734. );
  735. var dataURL = canvas.toDataURL('image/jpeg', 0.92);
  736. closeCropModal();
  737. uploadProfilePic(dataURL);
  738. }
  739. /* ================================================================
  740. Toast
  741. ================================================================ */
  742. var _toastTimer = null;
  743. function toast(type, title, message) {
  744. var el = document.getElementById('ua-toast');
  745. el.className = (_isDark ? 'dark' : '') + (type === 'error' ? ' error' : '');
  746. document.getElementById('ua-toast-title').textContent = title;
  747. document.getElementById('ua-toast-msg').textContent = message;
  748. el.style.display = 'block';
  749. if (_toastTimer) clearTimeout(_toastTimer);
  750. _toastTimer = setTimeout(function() { el.style.display = 'none'; }, 3500);
  751. }
  752. /* ================================================================
  753. Locale helper
  754. ================================================================ */
  755. function _s(key, def) {
  756. return (typeof userInfoApplocale !== 'undefined') ? userInfoApplocale.getString(key, def) : def;
  757. }
  758. </script>
  759. </body>
  760. </html>