ProfilesTab.vue 15 KB

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