ProfilesTab.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. <template>
  2. <div class="fit sets-tab-panel">
  3. <div class="sets-part-header">
  4. Управление синхронизацией данных
  5. </div>
  6. <div class="sets-item row">
  7. <div class="sets-label label"></div>
  8. <q-checkbox v-model="serverSyncEnabled" class="col" size="xs" label="Включить синхронизацию с сервером" />
  9. </div>
  10. <div v-show="serverSyncEnabled">
  11. <!---------------------------------------------->
  12. <div class="sets-part-header">
  13. Профили устройств
  14. </div>
  15. <div class="sets-item row">
  16. <div class="sets-label label"></div>
  17. <div class="text col">
  18. Выберите или добавьте профиль устройства, чтобы начать синхронизацию настроек с сервером.
  19. <br>При выборе "Нет" синхронизация настроек (но не книг) отключается.
  20. </div>
  21. </div>
  22. <div class="sets-item row">
  23. <div class="sets-label label">
  24. Устройство
  25. </div>
  26. <div class="col">
  27. <q-select
  28. v-model="currentProfile" :options="currentProfileOptions"
  29. style="width: 275px"
  30. bg-color="input"
  31. dropdown-icon="la la-angle-down la-sm"
  32. outlined dense emit-value map-options display-value-sanitize options-sanitize
  33. />
  34. </div>
  35. </div>
  36. <div class="sets-item row">
  37. <div class="sets-label label"></div>
  38. <q-btn class="sets-button" color="btn2" text-color="app" dense no-caps @click="addProfile">
  39. Добавить
  40. </q-btn>
  41. <q-btn class="sets-button" color="btn2" text-color="app" dense no-caps @click="delProfile">
  42. Удалить
  43. </q-btn>
  44. <q-btn class="sets-button" color="btn2" text-color="app" dense no-caps @click="delAllProfiles">
  45. Удалить все
  46. </q-btn>
  47. </div>
  48. <!---------------------------------------------->
  49. <div class="sets-part-header">
  50. Ключ доступа
  51. </div>
  52. <div class="sets-item row">
  53. <div class="sets-label label"></div>
  54. <div class="text col">
  55. Ключ доступа позволяет восстановить профили с настройками и список читаемых книг.
  56. Для этого необходимо передать ключ на новое устройство через почту, мессенджер или другим способом.
  57. </div>
  58. </div>
  59. <div class="sets-item row">
  60. <div class="sets-label label"></div>
  61. <q-btn class="sets-button" color="btn2" text-color="app" style="width: 250px" dense no-caps @click="showServerStorageKey">
  62. <span v-show="serverStorageKeyVisible">Скрыть</span>
  63. <span v-show="!serverStorageKeyVisible">Показать</span>
  64. &nbsp;ключ доступа
  65. </q-btn>
  66. </div>
  67. <div class="sets-item row">
  68. <div class="sets-label label"></div>
  69. <div v-if="!serverStorageKeyVisible" class="col">
  70. <hr />
  71. <b>{{ partialStorageKey }}</b> (часть вашего ключа)
  72. <hr />
  73. </div>
  74. <div v-else class="col" style="line-height: 100%">
  75. <hr />
  76. <div style="width: 300px; padding-top: 5px; overflow-wrap: break-word;">
  77. <b>{{ serverStorageKey }}</b>
  78. <q-icon class="copy-icon" name="la la-copy" @click="copyToClip(serverStorageKey, 'Ключ')">
  79. <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
  80. Скопировать
  81. </q-tooltip>
  82. </q-icon>
  83. </div>
  84. <div v-if="mode == 'omnireader' || mode == 'liberama'">
  85. <br>Переход по ссылке позволит автоматически ввести ключ доступа:
  86. <br><div class="text-center" style="margin-top: 5px">
  87. <a :href="setStorageKeyLink" target="_blank">Ссылка для ввода ключа</a>
  88. <q-icon class="copy-icon" name="la la-copy" @click="copyToClip(setStorageKeyLink, 'Ссылка')">
  89. <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
  90. Скопировать
  91. </q-tooltip>
  92. </q-icon>
  93. </div>
  94. </div>
  95. <hr />
  96. </div>
  97. </div>
  98. <div class="sets-item row">
  99. <div class="sets-label label"></div>
  100. <q-btn class="sets-button" color="btn2" text-color="app" style="width: 250px" dense no-caps @click="enterServerStorageKey">
  101. Ввести ключ доступа
  102. </q-btn>
  103. </div>
  104. <div class="sets-item row">
  105. <div class="sets-label label"></div>
  106. <q-btn class="sets-button" color="btn2" text-color="app" style="width: 250px" dense no-caps @click="generateServerStorageKey">
  107. Сгенерировать новый ключ
  108. </q-btn>
  109. </div>
  110. <div class="sets-item row">
  111. <div class="sets-label label"></div>
  112. <div class="text col">
  113. Рекомендуется сохранить ключ в надежном месте, чтобы всегда иметь возможность восстановить настройки,
  114. например, после переустановки ОС или чистки/смены браузера.<br>
  115. <b>ПРЕДУПРЕЖДЕНИЕ!</b> При утере ключа, НИКТО не сможет восстановить ваши данные, т.к. они сжимаются
  116. и шифруются ключом доступа перед отправкой на сервер.
  117. </div>
  118. </div>
  119. </div>
  120. </div>
  121. </template>
  122. <script>
  123. //-----------------------------------------------------------------------------
  124. import vueComponent from '../../../vueComponent.js';
  125. import _ from 'lodash';
  126. import * as utils from '../../../../share/utils';
  127. import rstore from '../../../../store/modules/reader';
  128. const componentOptions = {
  129. watch: {
  130. },
  131. };
  132. class ProfilesTab {
  133. _options = componentOptions;
  134. _props = {
  135. form: Object,
  136. };
  137. rstore = rstore;
  138. serverStorageKeyVisible = false;
  139. created() {
  140. this.commit = this.$store.commit;
  141. }
  142. mounted() {
  143. }
  144. get mode() {
  145. return this.$store.state.config.mode;
  146. }
  147. get serverSyncEnabled() {
  148. return this.$store.state.reader.serverSyncEnabled;
  149. }
  150. set serverSyncEnabled(newValue) {
  151. this.commit('reader/setServerSyncEnabled', newValue);
  152. }
  153. get currentProfile() {
  154. return this.$store.state.reader.currentProfile;
  155. }
  156. set currentProfile(newValue) {
  157. this.commit('reader/setCurrentProfile', newValue);
  158. }
  159. get profiles() {
  160. return this.$store.state.reader.profiles;
  161. }
  162. get currentProfileOptions() {
  163. const profNames = Object.keys(this.profiles)
  164. profNames.sort();
  165. let result = [{label: 'Нет', value: ''}];
  166. profNames.forEach(name => {
  167. result.push({label: name, value: name});
  168. });
  169. return result;
  170. }
  171. get partialStorageKey() {
  172. return this.serverStorageKey.substr(0, 7) + '***';
  173. }
  174. get serverStorageKey() {
  175. return this.$store.state.reader.serverStorageKey;
  176. }
  177. get setStorageKeyLink() {
  178. return `https://${window.location.host}/#/reader?setStorageAccessKey=${utils.toBase58(this.serverStorageKey)}`;
  179. }
  180. async addProfile() {
  181. try {
  182. if (Object.keys(this.profiles).length >= 100) {
  183. this.$root.stdDialog.alert('Достигнут предел количества профилей', 'Ошибка');
  184. return;
  185. }
  186. const result = await this.$root.stdDialog.prompt('Введите произвольное название для профиля устройства:', ' ', {
  187. inputValidator: (str) => { if (!str) return 'Название не должно быть пустым'; else if (str.length > 50) return 'Слишком длинное название'; else return true; },
  188. });
  189. if (result && result.value) {
  190. if (this.profiles[result.value]) {
  191. this.$root.stdDialog.alert('Такой профиль уже существует', 'Ошибка');
  192. } else {
  193. const newProfiles = Object.assign({}, this.profiles, {[result.value]: 1});
  194. this.commit('reader/setAllowProfilesSave', true);
  195. await this.$nextTick();//ждем обработчики watch
  196. this.commit('reader/setProfiles', newProfiles);
  197. await this.$nextTick();//ждем обработчики watch
  198. this.commit('reader/setAllowProfilesSave', false);
  199. this.currentProfile = result.value;
  200. }
  201. }
  202. } catch (e) {
  203. //
  204. }
  205. }
  206. async delProfile() {
  207. if (!this.currentProfile)
  208. return;
  209. try {
  210. const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.$root.sanitize(this.currentProfile)}' необратимо.` +
  211. `<br>Все настройки профиля будут потеряны, однако список читаемых книг сохранится.` +
  212. `<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
  213. inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
  214. });
  215. if (result && result.value && result.value.toLowerCase() == 'да') {
  216. if (this.profiles[this.currentProfile]) {
  217. const newProfiles = Object.assign({}, this.profiles);
  218. delete newProfiles[this.currentProfile];
  219. this.commit('reader/setAllowProfilesSave', true);
  220. await this.$nextTick();//ждем обработчики watch
  221. this.commit('reader/setProfiles', newProfiles);
  222. await this.$nextTick();//ждем обработчики watch
  223. this.commit('reader/setAllowProfilesSave', false);
  224. this.currentProfile = '';
  225. }
  226. }
  227. } catch (e) {
  228. //
  229. }
  230. }
  231. async delAllProfiles() {
  232. if (!Object.keys(this.profiles).length)
  233. return;
  234. try {
  235. const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление ВСЕХ профилей с настройками необратимо.` +
  236. `<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
  237. inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
  238. });
  239. if (result && result.value && result.value.toLowerCase() == 'да') {
  240. this.commit('reader/setAllowProfilesSave', true);
  241. await this.$nextTick();//ждем обработчики watch
  242. this.commit('reader/setProfiles', {});
  243. await this.$nextTick();//ждем обработчики watch
  244. this.commit('reader/setAllowProfilesSave', false);
  245. this.currentProfile = '';
  246. }
  247. } catch (e) {
  248. //
  249. }
  250. }
  251. async showServerStorageKey() {
  252. this.serverStorageKeyVisible = !this.serverStorageKeyVisible;
  253. }
  254. async enterServerStorageKey(key) {
  255. try {
  256. const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Изменение ключа доступа приведет к замене всех профилей и читаемых книг в читалке.` +
  257. `<br><br>Введите новый ключ доступа:`, ' ', {
  258. inputValidator: (str) => {
  259. try {
  260. if (str && utils.fromBase58(str).length == 32) {
  261. return true;
  262. }
  263. } catch (e) {
  264. //
  265. }
  266. return 'Неверный формат ключа';
  267. },
  268. inputValue: (key && _.isString(key) ? key : null),
  269. });
  270. if (result && result.value && utils.fromBase58(result.value).length == 32) {
  271. this.commit('reader/setServerStorageKey', result.value);
  272. }
  273. } catch (e) {
  274. //
  275. }
  276. }
  277. async generateServerStorageKey() {
  278. try {
  279. const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Генерация нового ключа доступа приведет к удалению всех профилей и читаемых книг в читалке.` +
  280. `<br><br>Введите 'да' для подтверждения генерации нового ключа:`, ' ', {
  281. inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Генерация не подтверждена'; },
  282. });
  283. if (result && result.value && result.value.toLowerCase() == 'да') {
  284. if (this.$root.generateNewServerStorageKey)
  285. this.$root.generateNewServerStorageKey();
  286. }
  287. } catch (e) {
  288. //
  289. }
  290. }
  291. async copyToClip(text, prefix) {
  292. const result = await utils.copyTextToClipboard(text);
  293. const suf = (prefix.substr(-1) == 'а' ? 'а' : '');
  294. const msg = (result ? `${prefix} успешно скопирован${suf} в буфер обмена` : 'Копирование не удалось');
  295. if (result)
  296. this.$root.notify.success(msg);
  297. else
  298. this.$root.notify.error(msg);
  299. }
  300. }
  301. export default vueComponent(ProfilesTab);
  302. //-----------------------------------------------------------------------------
  303. </script>
  304. <style scoped>
  305. .label {
  306. width: 75px;
  307. }
  308. .text {
  309. font-size: 90%;
  310. line-height: 130%;
  311. }
  312. .copy-icon {
  313. margin-left: 5px;
  314. cursor: pointer;
  315. font-size: 120%;
  316. color: var(--text-anchor-color);
  317. }
  318. </style>