简化版的mastodon web client
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

443 lines
12 KiB

2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  6. <link rel="icon" type="image/png" href="/img/ord/icon-128.png" />
  7. <meta property="og:title" content="闭社简化版" />
  8. <meta property="og:description" content="一个提供更简洁界面的闭社web client" />
  9. <link href="https://cdn.bootcss.com/twitter-bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
  10. <title>闭社简化版</title>
  11. <style>
  12. body {
  13. background: linear-gradient(-45deg, #fff calc(50% - 1px), #d0d0d0 calc(50%), #fff calc(50% + 1px) );
  14. background-size: 6px 5px;
  15. font-family: 'Noto Sans SC', sans-serif;
  16. }
  17. body.grey {
  18. background: #707070;
  19. background-size: 6px 5px;
  20. }
  21. body,
  22. pre {
  23. font-family: 'Noto Sans SC', sans-serif;
  24. }
  25. h1,
  26. h2,
  27. h3,
  28. h4,
  29. h5,
  30. h6 {
  31. font-family: 'Noto Serif SC', serif;
  32. font-weight: 300;
  33. }
  34. a,
  35. a:hover,
  36. .btn-link,
  37. .btn-link:hover {
  38. color: inherit;
  39. text-decoration: underline;
  40. display: inline-block;
  41. }
  42. .part1 {
  43. min-width: 300px;
  44. float: left;
  45. padding-right: 10px;
  46. position: relative;
  47. }
  48. .part2 {
  49. min-width: 200px;
  50. overflow: hidden;
  51. padding-left: 25px;
  52. }
  53. .qbox {
  54. border: 2px black solid;
  55. background: white;
  56. padding: 5px;
  57. color: black;
  58. margin: 5px 5px 20px;
  59. }
  60. body.grey #statuses-list .qbox {
  61. background: #b0b0b0;
  62. }
  63. .new .qbox {
  64. background: black;
  65. color: white;
  66. }
  67. .new .qbox textarea {
  68. background: black;
  69. color: white;
  70. }
  71. .qbox textarea {
  72. border: none;
  73. border-bottom: 1px solid;
  74. border-radius: 0;
  75. }
  76. body.grey #statuses-list .qbox textarea {
  77. background: #b0b0b0;
  78. }
  79. .qbox .content {
  80. margin: 15px 5px;
  81. }
  82. .liked, .reblogged {
  83. font-weight: bold;
  84. }
  85. .timeago {
  86. font-size: 0.5em;
  87. }
  88. .display_name {
  89. margin: 0;
  90. }
  91. .behind {
  92. z-index: 98;
  93. cursor: pointer;
  94. transform: translateY(5px) scale(0.98);
  95. transform-origin: top;
  96. transition-property: transform;
  97. transition-duration: 0.5s;
  98. }
  99. .front {
  100. z-index: 99;
  101. transition-property: transform;
  102. transition-duration: 0.5s;
  103. }
  104. .judge {
  105. position: absolute;
  106. top: 0;
  107. right: 0;
  108. margin: 0 0 30px 20px;
  109. width: 90%;
  110. }
  111. .new {
  112. position: relative;
  113. margin: 30px 20px 30px 0;
  114. }
  115. a.hashtag {
  116. font-weight: bold;
  117. }
  118. .comment-list-wrapper {
  119. background: #3333;
  120. padding-left: 7%;
  121. font-size: 90%;
  122. }
  123. .status-media {
  124. max-height: 500px;
  125. max-width: 100%;
  126. margin: 10px auto;
  127. }
  128. .emoji {
  129. width: 22px;
  130. vertical-align: text-bottom;
  131. }
  132. #statuses-list .invisible {
  133. font-size: 0;
  134. line-height: 0;
  135. display: inline-block;
  136. width: 0;
  137. height: 0;
  138. visibility: visible !important;
  139. }
  140. #statuses-list .ellipsis::after {
  141. content: "...";
  142. }
  143. </style>
  144. </head>
  145. <body>
  146. <div class="container" style="overflow: hidden;min-height: 100vh">
  147. <div style='padding:15px'>
  148. <h1> 闭社简化版 </h1>
  149. </div>
  150. <div class="part1">
  151. <div class="new">
  152. <form action="" onsubmit="return post_status(event, '')">
  153. <div class="form-group qbox">
  154. <textarea class="form-control" name="text" rows="5" maxlength="5000" placeholder="啥?" required="required"></textarea>
  155. <div class="form-check mt-3 mb-3">
  156. <input class="form-check-input" type="checkbox" value="" id="post-checkbox-an">
  157. <label class="form-check-label" for="post-checkbox-an">
  158. 匿名
  159. </label>
  160. </div>
  161. <button type="submit" class="btn btn-link btn-lg">发布</button>
  162. </div>
  163. </form>
  164. </div>
  165. </div>
  166. <div class="part2" id="part2">
  167. <h1>本站</h1>
  168. <button class="btn btn-link grey_theme">切换亮度</button>
  169. <div id="statuses-list">
  170. </div>
  171. <span id="loading-span">加载中..</span>
  172. </div>
  173. </div>
  174. </body>
  175. <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
  176. <script src="https://cdn.bootcss.com/twitter-bootstrap/4.3.1/js/bootstrap.min.js"></script>
  177. <script src="https://cdn.bootcdn.net/ajax/libs/jquery-timeago/1.6.7/jquery.timeago.min.js"></script>
  178. <script src="https://cdn.bootcdn.net/ajax/libs/jquery-timeago/1.6.7/locales/jquery.timeago.zh-CN.js"></script>
  179. <script>
  180. var token;
  181. var max_id;
  182. var loading_statues;
  183. var base_api_url = "https://thu.closed.social/";
  184. function get_token() {
  185. (document.cookie || "").split("; ").forEach( (c) => {
  186. let cc = c.split("=");
  187. if (cc.shift() === "mast_token") {
  188. token = cc.join("=");
  189. }
  190. });
  191. }
  192. function get_more_statuses() {
  193. loading_statues = true;
  194. $.getJSON(
  195. `${base_api_url}api/v1/timelines/public?limit=20&local=true` + (
  196. max_id ? `&max_id=${max_id}` : ''
  197. ),
  198. function (data) {
  199. data.forEach((status) => {
  200. $('#statuses-list').append(make_status_box_html(status, true));
  201. });
  202. $('.timeago').timeago();
  203. if (data.length) {
  204. max_id = data[data.length - 1].id;
  205. loading_statues = false;
  206. }
  207. }
  208. ).fail((e) => {
  209. alert(e.responseText);
  210. });
  211. }
  212. function render_content(text, is_mask_bot, emojis) {
  213. if (is_mask_bot) {
  214. text = text.replace(/^<p>\[([^\]]*)\]:<br \/>/, '<p>').replace(/匿了<\/p>$/, '</p>');
  215. }
  216. emojis.forEach((emoji) => {
  217. text = text.replaceAll(`:${emoji.shortcode}:`, `<img class="emoji" src="${emoji.url}">`);
  218. });
  219. return text;
  220. }
  221. function make_status_box_html(status) {
  222. return (`
  223. <div class="qbox" id="status-${status.id.toString()}">
  224. <img class="avatar" width="24" src="${status.account.avatar}">
  225. <small>
  226. ${status.account.acct === "mask_bot" ?
  227. "匿名用户" + /^<p>(\[[^\]]*\]):/.exec(status.content)[1] : (
  228. status.account.display_name + ' @' +status.account.acct)}
  229. </small>
  230. ${status.reblog && (status = status.reblog) &&
  231. `<small> 转发 @${status.account.acct}</small>` || ''}
  232. <div class="content">
  233. ${render_content(status.content, status.account.acct === "mask_bot", status.emojis)}
  234. </div>
  235. ${status.media_attachments.map((media) => {
  236. switch (media.type) {
  237. case 'image':
  238. return `<image class="status-media" src=${media.url}>`;
  239. case 'video':
  240. return `<video class="status-media" src=${media.url} controls></video>`;
  241. case 'gifv':
  242. return `<video class="status-media" src=${media.url} autoplay loop></video>`;
  243. default:
  244. return '';
  245. }
  246. }).join('\n')}
  247. <div style="text-align:right;margin: 0px 0 -5px">
  248. <time class="timeago mr-1" datetime="${status.created_at}"
  249. title="${status.created_at}"></time>
  250. <small class="mr-2">${status.application && status.application.name || ''}</small>
  251. <a href="###" class="mr-3 like-btn ${status.favourited? 'liked' : ''}"
  252. onclick="like_status(event, '${status.id.toString()}')">
  253. 点赞<span class="num">${status.favourites_count}</span>
  254. </a>
  255. <a href="###" class="mr-3 reblog-btn ${status.reblogged? 'reblogged' : ''}"
  256. onclick="reblog_status(event, '${status.id.toString()}')">
  257. 转发<span class="num">${status.reblogs_count}</span>
  258. </a>
  259. <a class="mr-3 reply-btn" href="###"
  260. onclick="get_comments('${status.id.toString()}')">
  261. 回复${status.replies_count}
  262. </a>
  263. </div>
  264. <div class="collapse comment-list-wrapper" id="collapse-${status.id.toString()}">
  265. <div class="comment-list">
  266. </div>
  267. <form action="" onsubmit="return post_status(event, '${status.id}')">
  268. <div class="form-group qbox">
  269. <textarea class="form-control" rows="2" maxlength="5000" required="required" >@${status.account.acct} </textarea>
  270. <div class="form-check mt-1 mb-1">
  271. <input class="form-check-input" type="checkbox" value="" id="reply-${status.id}-checkbox-an">
  272. <label class="form-check-label" for="reply-${status.id}-checkbox-an">
  273. 匿名
  274. </label>
  275. </div>
  276. <button type="submit" class="btn btn-link">添加回复</button>
  277. </div>
  278. </form>
  279. </div>
  280. </div>
  281. `)
  282. }
  283. function get_comments(sid) {
  284. let coll = $(`#collapse-${sid}`);
  285. if (coll.hasClass('show')) {
  286. coll.collapse('hide');
  287. } else {
  288. coll.collapse('show');
  289. coll.find('> .comment-list').append("加载中..");
  290. $.getJSON(
  291. `${base_api_url}api/v1/statuses/${sid}/context`,
  292. function (data) {
  293. coll.find('> .comment-list').empty();
  294. data.descendants.forEach( (rp) => {
  295. let parent_coll = $(`#collapse-${rp.in_reply_to_id}`);
  296. parent_coll.collapse('show');
  297. parent_coll.find('> .comment-list').append(make_status_box_html(rp));
  298. });
  299. $('.timeago').timeago();
  300. }
  301. );
  302. }
  303. }
  304. function like_status(e, sid) {
  305. let target = $(e.target);
  306. if (!target.hasClass('liked')) {
  307. $.post(
  308. `${base_api_url}api/v1/statuses/${sid}/favourite`,
  309. (status) => {
  310. target.addClass('liked');
  311. target.find('.num').text(status.favourites_count)
  312. }
  313. );
  314. } else {
  315. $.post(
  316. `${base_api_url}api/v1/statuses/${sid}/unfavourite`,
  317. (status) => {
  318. target.removeClass('liked');
  319. target.find('.num').text(
  320. target.find('.num').text() - 1
  321. )
  322. }
  323. );
  324. }
  325. }
  326. function reblog_status(e, sid) {
  327. let target = $(e.target);
  328. if (!target.hasClass('reblogged')) {
  329. $.post(
  330. `${base_api_url}api/v1/statuses/${sid}/reblog`,
  331. (status) => {
  332. target.addClass('reblogged');
  333. target.find('.num').text(
  334. target.find('.num').text() - (-1)
  335. )
  336. }
  337. );
  338. } else {
  339. $.post(
  340. `${base_api_url}api/v1/statuses/${sid}/unreblog`,
  341. (status) => {
  342. target.removeClass('reblogged');
  343. target.find('.num').text(
  344. target.find('.num').text() - 1
  345. )
  346. }
  347. );
  348. }
  349. }
  350. function post_status(e, sid) {
  351. e.preventDefault();
  352. let form = $(e.target);
  353. let text = form.find('textarea').val();
  354. if (form.find('input[type=checkbox]').is(':checked')) {
  355. text += "\n匿了";
  356. }
  357. $.post(
  358. `${base_api_url}api/v1/statuses`,
  359. {'status': text, 'in_reply_to_id': sid || null, 'visibility': 'public'},
  360. (status) => {
  361. form.find('textarea').val('');
  362. if (sid) {
  363. form.prev().append(make_status_box_html(status));
  364. } else {
  365. $('#statuses-list').prepend(make_status_box_html(status));
  366. }
  367. },
  368. 'json'
  369. ).fail((e) => {
  370. alert(e.responseText);
  371. });
  372. return false;
  373. }
  374. $(document).ready(function(){
  375. get_token();
  376. console.log(token);
  377. if (!token) {
  378. location.href = `${base_api_url}oauth/authorize?client_id=Wjf6ajif5kl6rIIt_TLu7SAAluGskaQiTXZoIr44jUc&response_type=code&redirect_uri=${encodeURIComponent(location.origin + "/auth")}&scope=read+write&force_login=False`
  379. } else {
  380. $.ajaxSetup({
  381. headers : {
  382. 'Authorization' : 'Bearer ' + token
  383. }
  384. });
  385. get_more_statuses();
  386. $(window).scroll(() => {
  387. if($('#loading-span').offset().top < $(window).scrollTop() + $(window).innerHeight() * 1.5 && !loading_statues) {
  388. get_more_statuses();
  389. }
  390. });
  391. }
  392. $('.grey_theme').click((e) => {
  393. $('body').toggleClass("grey");
  394. });
  395. });
  396. </script>
  397. </html>