├── assets
├── bg.png
├── eye.png
├── icon.png
├── user.png
├── .DS_Store
├── sspai.png
├── lightning.png
├── writing.png
└── power-plus.png
├── strings
├── zh-Hans.strings
└── en.strings
├── .output
└── Namecard for sspai.box
├── .gitignore
├── config.json
├── scripts
├── requests.js
└── app.js
├── LICENSE
├── main.js
└── README.md
/assets/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spencerwooo/jsbox-sspai-namecard/HEAD/assets/bg.png
--------------------------------------------------------------------------------
/assets/eye.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spencerwooo/jsbox-sspai-namecard/HEAD/assets/eye.png
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spencerwooo/jsbox-sspai-namecard/HEAD/assets/icon.png
--------------------------------------------------------------------------------
/assets/user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spencerwooo/jsbox-sspai-namecard/HEAD/assets/user.png
--------------------------------------------------------------------------------
/assets/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spencerwooo/jsbox-sspai-namecard/HEAD/assets/.DS_Store
--------------------------------------------------------------------------------
/assets/sspai.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spencerwooo/jsbox-sspai-namecard/HEAD/assets/sspai.png
--------------------------------------------------------------------------------
/strings/zh-Hans.strings:
--------------------------------------------------------------------------------
1 | "FOLLOWING" = "关注";
2 | "FOLLOWERS" = "关注者";
3 | "ACHIEVEMENTS" = "成就";
4 |
--------------------------------------------------------------------------------
/assets/lightning.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spencerwooo/jsbox-sspai-namecard/HEAD/assets/lightning.png
--------------------------------------------------------------------------------
/assets/writing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spencerwooo/jsbox-sspai-namecard/HEAD/assets/writing.png
--------------------------------------------------------------------------------
/assets/power-plus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spencerwooo/jsbox-sspai-namecard/HEAD/assets/power-plus.png
--------------------------------------------------------------------------------
/strings/en.strings:
--------------------------------------------------------------------------------
1 | "FOLLOWING" = "Following";
2 | "FOLLOWERS" = "Followers";
3 | "ACHIEVEMENTS" = "Achievements";
--------------------------------------------------------------------------------
/.output/Namecard for sspai.box:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spencerwooo/jsbox-sspai-namecard/HEAD/.output/Namecard for sspai.box
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # General
2 | .DS_Store
3 | .AppleDouble
4 | .LSOverride
5 |
6 | # Icon must end with two \r
7 | Icon
8 |
9 |
10 | # Thumbnails
11 | ._*
12 |
13 | # Files that might appear in the root of a volume
14 | .DocumentRevisions-V100
15 | .fseventsd
16 | .Spotlight-V100
17 | .TemporaryItems
18 | .Trashes
19 | .VolumeIcon.icns
20 | .com.apple.timemachine.donotpresent
21 |
22 | # Directories potentially created on remote AFP share
23 | .AppleDB
24 | .AppleDesktop
25 | Network Trash Folder
26 | Temporary Items
27 | .apdisk
28 |
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "info": {
3 | "name": "少数派作者名片",
4 | "url": "https://github.com/spencerwooo/jsbox-sspai-namecard",
5 | "version": "1.0.0",
6 | "author": "SpencerWoo",
7 | "website": "https://github.com/spencerwooo/jsbox-sspai-namecard",
8 | "types": 0
9 | },
10 | "settings": {
11 | "minSDKVer": "1.0.0",
12 | "minOSVer": "10.0.0",
13 | "idleTimerDisabled": false,
14 | "autoKeyboardEnabled": false,
15 | "keyboardToolbarEnabled": false,
16 | "rotateDisabled": false
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/scripts/requests.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 处理 API 请求
3 | */
4 |
5 | // 少数派用户信息接口更新频繁,可能会有查询问题,请注意及时更新脚本
6 | var sspaiApiUrl = 'https://beta.sspai.com/api/v1/user/slug/info/get?slug='
7 |
8 | var sspaiSearchApi =
9 | 'https://beta.sspai.com/api/v1/user/search/page/get?limit=1&nickname='
10 |
11 | async function getUserId(sspaiSearchName) {
12 | let resp = await $http.get({
13 | url: sspaiSearchApi + sspaiSearchName
14 | })
15 | $console.info(resp.data)
16 | return resp.data
17 | }
18 |
19 | async function getUserInfo(sspaiUserId) {
20 | let resp = await $http.get({
21 | url: sspaiApiUrl + sspaiUserId
22 | })
23 | $console.info(resp.data)
24 | return resp.data
25 | }
26 |
27 | module.exports = {
28 | getUserInfo: getUserInfo,
29 | getUserId: getUserId
30 | }
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Spencer Woo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | var app = require('scripts/app')
2 | var requests = require('scripts/requests')
3 |
4 | // var sspaiUserName = 'SpencerWoo'
5 |
6 | // 请在这里将 userId 的值替换为你的少数派用户 ID,在你的少数派用户主页链接中即可找到
7 | // 比如:https://beta.sspai.com/user/spencerwoo/updates 中, spencerwoo 即为少数派 userId
8 | var userId = 'spencerwoo'
9 |
10 | function main() {
11 | // 通过搜索接口获取少数派用户 ID,目前访问速度非常慢,以后再说
12 | // requests.getUserId(sspaiUserName).then(resp => {
13 | // let userId = resp.data.data[0].id
14 |
15 | // })
16 |
17 | // 将 ID 传递给获取用户信息接口
18 | requests.getUserInfo(userId).then(resp => {
19 | if (resp.error === 0) {
20 | // API 接口正常
21 | let userInfo = resp.data
22 | // $console.info(userInfo)
23 |
24 | app.renderUI(userInfo)
25 | } else {
26 | $ui.alert({
27 | title: '⚠️ API 出现问题',
28 | message: '少数派用户信息接口错误,检查是否联网或重新安装最新脚本',
29 | actions: [
30 | {
31 | title: '好的',
32 | handler: function() {
33 | $app.close()
34 | }
35 | },
36 | {
37 | title: '前往脚本 Release 页',
38 | handler: function() {
39 | $app.openURL(
40 | 'https://github.com/spencerwooo/jsbox-sspai-namecard/releases'
41 | )
42 | }
43 | }
44 | ]
45 | })
46 | }
47 | })
48 | }
49 |
50 | main()
51 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 | # 少数派名片 for JSBox
6 |
7 | > 少数派作者名片 | 一个 JSBox 小组件
8 |
9 | **推荐阅读:**[新版少数派网站的作者成就墙好好看啊,于是我用它写了一个 JSBox 小插件](https://beta.sspai.com/post/55562)
10 |
11 | ## Screenshots
12 |
13 | ### Interface 界面
14 |
15 | 
16 |
17 | 按照新版少数派 API,显示了少数派作者:
18 |
19 | - 用户名、头像
20 | - 关注与关注者
21 | - 获得勋章
22 | - 用户简介
23 | - 以及获得成就
24 |
25 | ### Badges 勋章墙
26 |
27 | 
28 |
29 | ### Animations 动画
30 |
31 |
32 |

33 |
34 |
35 | ## 安装
36 |
37 | 顾名思义,你需要先在 iOS 上购买 JSBox。
38 |
39 | 然后,用 Safari 浏览器打开:[Namecard for sspai 的安装链接](https://xteko.com/redir?name=Namecard%20for%20sspai&url=https://github.com/spencerwooo/jsbox-sspai-namecard/releases/download/v0.2.0/Namecard-for-sspai.box) 来安装脚本。
40 |
41 | ## 使用
42 |
43 | > 少数派主站在更新迭代,最近 API 可能有频繁变化,请大家如果发现脚本失效或其他问题及时给我提 issue。也请大家密切关注发布页:[jsbox-sspai-namecard/releases](https://github.com/spencerwooo/jsbox-sspai-namecard/releases),新版脚本我会及时发布在这里。
44 |
45 | 查看脚本源码,找到目录下的 `main.js`,将第八行:
46 |
47 | ```javascript
48 | var userId = 'spencerwoo'
49 | ```
50 |
51 | 后面的数字替换为你的少数派用户 ID,通常在你的少数派个人主页的链接里面就可以找到,比如 `https://beta.sspai.com/u/spencerwoo/updates` 里面的 `spencerwoo` 就是我的少数派用户 ID。
52 |
53 | 推荐将脚本设置为通知中心小组件,以 Today Widget(小组件)的形式使用。
54 |
55 | 推荐将 Today Widget(小组件)的高度设置为 240。
56 |
57 | ## 免责
58 |
59 | **少数派作者名片 for JSBox** 和少数派官方无关,只作为 JSBox 中的展示作者信息的途径。
60 |
61 | ---
62 |
63 | 📟 **Namecard for sspai** ©Spencer Woo. Released under the MIT License.
64 |
65 | Authored and maintained by Spencer Woo.
66 |
67 | [@Portfolio](https://spencerwoo.com/) · [@Blog](https://blog.spencerwoo.com/) · [@GitHub](https://github.com/spencerwooo)
68 |
--------------------------------------------------------------------------------
/scripts/app.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 渲染 app 界面
3 | */
4 |
5 | var defaultAssetPath = 'https://cdn.sspai.com/'
6 |
7 | function getRegisteredDays(creationDay) {
8 | // 返回的注册日期是 unix timestamp,例子:1515163617,单位是秒
9 | let registerDay = new Date(creationDay * 1000)
10 | let today = new Date()
11 | let diff = Math.abs((today - registerDay) / 1000 / 60 / 60 / 24)
12 | return Math.floor(diff)
13 | }
14 |
15 | function renderUI(userInfo) {
16 | // Avatar animation cycles
17 | let cycles = 1
18 | // Icon tapped boolean
19 | let iconTapped = 0
20 |
21 | // Render UI elements
22 | $ui.render({
23 | props: {
24 | title: '少数派作者名片'
25 | },
26 | views: [
27 | {
28 | type: 'view',
29 | props: {
30 | id: 'app',
31 | bgcolor: $color('#F9F9F9'),
32 | radius: 10
33 | },
34 | layout: function(make, view) {
35 | make.top.equalTo(view.super).inset(5)
36 | make.left.right.bottom.equalTo(view.super).inset(10)
37 | }
38 | },
39 | {
40 | type: 'image',
41 | props: {
42 | id: 'background',
43 | src: 'assets/bg.png',
44 | alpha: 0.1,
45 | radius: 10
46 | },
47 | layout: function(make, view) {
48 | make.top.right.equalTo($('app'))
49 | make.size.equalTo($size(300, 91.5))
50 | }
51 | },
52 | {
53 | type: 'views',
54 | props: {
55 | id: 'avatar-container'
56 | },
57 | views: [
58 | {
59 | type: 'image',
60 | props: {
61 | id: 'avatar',
62 | src: defaultAssetPath + userInfo.avatar,
63 | radius: 32
64 | },
65 | layout: function(make) {
66 | make.size.equalTo($size(64, 64))
67 | },
68 | events: {
69 | tapped: function() {
70 | $device.taptic(1)
71 | $ui.animate({
72 | duration: 0.4,
73 | delay: 0,
74 | damping: 1,
75 | velocity: 0,
76 | options: 0,
77 | animation: function() {
78 | $('avatar').rotate(Math.PI * cycles)
79 | cycles = cycles + 1
80 | }
81 | })
82 | }
83 | }
84 | }
85 | ],
86 | layout: function(make, view) {
87 | make.top.equalTo(view.super).inset(18)
88 | make.right.equalTo(view.super).inset(22)
89 | make.size.equalTo($size(64, 64))
90 | }
91 | },
92 | {
93 | type: 'image',
94 | props: {
95 | id: 'sspai-icon',
96 | src: 'assets/icon.png'
97 | },
98 | layout: function(make, view) {
99 | make.top.inset(18)
100 | make.left.inset(25)
101 | make.size.equalTo($size(25, 25))
102 | },
103 | events: {
104 | tapped: function() {
105 | $device.taptic(1)
106 |
107 | if (iconTapped === 0) {
108 | $ui.animate({
109 | duration: 0.4,
110 | animation: function() {
111 | $('background').alpha = 0.9
112 | }
113 | })
114 | iconTapped = 1
115 | } else {
116 | $ui.animate({
117 | duration: 0.4,
118 | animation: function() {
119 | $('background').alpha = 0.1
120 | }
121 | })
122 | iconTapped = 0
123 | }
124 | }
125 | }
126 | },
127 | {
128 | type: 'label',
129 | props: {
130 | id: 'nickname',
131 | text: userInfo.nickname,
132 | font: $font('Menlo-Bold', 20),
133 | color: $color('#24292E'),
134 | align: $align.left
135 | },
136 | layout: function(make, view) {
137 | make.top.equalTo(view.super).inset(18)
138 | make.left.equalTo(view.super).inset(60)
139 | }
140 | },
141 | {
142 | type: 'label',
143 | props: {
144 | id: 'follower-stats',
145 | text:
146 | $l10n('FOLLOWING') +
147 | ' ' +
148 | userInfo.following_count +
149 | ' · ' +
150 | $l10n('FOLLOWERS') +
151 | ' ' +
152 | userInfo.followed_count,
153 | font: $font(12),
154 | color: $color('#777777'),
155 | align: $align.left
156 | },
157 | layout: function(make, view) {
158 | make.left.inset(25)
159 | make.top.equalTo($('nickname').bottom).offset(8)
160 | }
161 | },
162 | {
163 | type: 'label',
164 | props: {
165 | id: 'bio',
166 | text: userInfo.bio.split('\n').join(' '),
167 | font: $font(12),
168 | color: $color('#24292E'),
169 | align: $align.left
170 | },
171 | layout: function(make, view) {
172 | make.left.inset(25)
173 | make.top.equalTo($('follower-stats').bottom).offset(5)
174 | make.width.equalTo(280)
175 | }
176 | },
177 | {
178 | type: 'views',
179 | views: [
180 | {
181 | type: 'view',
182 | props: {
183 | id: 'details',
184 | bgcolor: $color('#292525'),
185 | radius: 10
186 | },
187 | layout: function(make, view) {
188 | make.top.left.right.bottom.equalTo(view.super)
189 | }
190 | },
191 | {
192 | type: 'label',
193 | props: {
194 | id: 'achievements-label',
195 | text: $l10n('ACHIEVEMENTS'),
196 | font: $font('Menlo-Bold', 18),
197 | color: $color('#ffffff')
198 | },
199 | layout: function(make, view) {
200 | make.top.equalTo(view.super).inset(25)
201 | make.left.inset(20)
202 | }
203 | },
204 | {
205 | type: 'image',
206 | props: {
207 | id: 'writing-icon',
208 | src: 'assets/writing.png'
209 | },
210 | layout: function(make, view) {
211 | make.top.equalTo($('achievements-label').bottom).offset(15)
212 | make.left.offset(20)
213 | make.size.equalTo($size(15, 15))
214 | }
215 | },
216 | {
217 | type: 'label',
218 | props: {
219 | id: 'writing-label',
220 | text:
221 | '写作 ' + userInfo.articles_word_count.toLocaleString() + ' 字',
222 | font: $font(12),
223 | color: $color('#ffffff')
224 | },
225 | layout: function(make, view) {
226 | make.top.equalTo($('achievements-label').bottom).offset(15)
227 | make.left.equalTo($('writing-icon').right).offset(10)
228 | }
229 | },
230 | {
231 | type: 'image',
232 | props: {
233 | id: 'lightning-icon',
234 | src: 'assets/lightning.png'
235 | },
236 | layout: function(make, view) {
237 | make.top.equalTo($('achievements-label').bottom).offset(15)
238 | make.left.offset(200)
239 | make.size.equalTo($size(15, 15))
240 | }
241 | },
242 | {
243 | type: 'label',
244 | props: {
245 | text: '获得 ' + userInfo.liked_count.toLocaleString() + ' 能量',
246 | font: $font(12),
247 | color: $color('#ffffff')
248 | },
249 | layout: function(make, view) {
250 | make.top.equalTo($('achievements-label').bottom).offset(15)
251 | make.left.equalTo($('lightning-icon').right).offset(10)
252 | }
253 | },
254 | {
255 | type: 'image',
256 | props: {
257 | id: 'eye-icon',
258 | src: 'assets/eye.png'
259 | },
260 | layout: function(make, view) {
261 | make.top.equalTo($('writing-label').bottom).offset(15)
262 | make.left.offset(20)
263 | make.size.equalTo($size(15, 15))
264 | }
265 | },
266 | {
267 | type: 'label',
268 | props: {
269 | id: 'eye-label',
270 | text:
271 | '文章被阅读 ' +
272 | userInfo.article_view_count.toLocaleString() +
273 | ' 次',
274 | font: $font(12),
275 | color: $color('#ffffff')
276 | },
277 | layout: function(make, view) {
278 | make.top.equalTo($('writing-label').bottom).offset(15)
279 | make.left.equalTo($('eye-icon').right).offset(10)
280 | }
281 | },
282 | {
283 | type: 'image',
284 | props: {
285 | id: 'user-icon',
286 | src: 'assets/user.png'
287 | },
288 | layout: function(make, view) {
289 | make.top.equalTo($('writing-label').bottom).offset(15)
290 | make.left.offset(200)
291 | make.size.equalTo($size(15, 15))
292 | }
293 | },
294 | {
295 | type: 'label',
296 | props: {
297 | text:
298 | '成为少数派 ' +
299 | getRegisteredDays(userInfo.created_at).toLocaleString() +
300 | ' 天',
301 | font: $font(12),
302 | color: $color('#ffffff')
303 | },
304 | layout: function(make, view) {
305 | make.top.equalTo($('writing-label').bottom).offset(15)
306 | make.left.equalTo($('user-icon').right).offset(10)
307 | }
308 | }
309 | ],
310 | layout: function(make, view) {
311 | make.top.equalTo($('avatar-container').bottom).offset(15)
312 | make.left.right.inset(10)
313 | make.bottom.equalTo(view.super).inset(10)
314 | }
315 | }
316 | ]
317 | })
318 |
319 | // 拥有勋章:
320 | // 1. 签约作者、专业作者、少数派成员等等
321 | // 最右侧 badge 距离头像 12 初始距离
322 | let insetMargin = 12
323 |
324 | if (userInfo.user_flags.length > 0) {
325 | let flagLabelMargin = insetMargin + userInfo.user_flags.length * 30
326 | // 显示勋章信息的 label
327 | $('app').add({
328 | type: 'label',
329 | props: {
330 | id: 'flags-label',
331 | text: 'label',
332 | alpha: 0,
333 | font: $font(14),
334 | color: $color('#777777')
335 | },
336 | layout: function(make, view) {
337 | make.top.equalTo(view.super).inset(16)
338 | make.left.equalTo($('nickname').right).offset(flagLabelMargin)
339 | }
340 | })
341 |
342 | // 勋章点击与否
343 | let tapped = 0
344 |
345 | userInfo.user_flags.forEach(flag => {
346 | // 在头像 avatar 左侧每隔 30 距离添加一个 badge
347 | $('app').add({
348 | type: 'image',
349 | props: {
350 | src: flag.icon
351 | },
352 | layout: function(make, view) {
353 | make.size.equalTo($size(18, 20))
354 | make.top.equalTo(view.super).inset(15)
355 | make.left.equalTo($('nickname').right).offset(insetMargin)
356 | },
357 | events: {
358 | tapped: function() {
359 | $device.taptic(1)
360 |
361 | // 显示勋章名称
362 | $ui.animate({
363 | duration: 0.4,
364 | delay: 0,
365 | damping: 0,
366 | velocity: 0,
367 | options: 0,
368 | animation: function() {
369 | if (tapped === 0) {
370 | $('flags-label').text = flag.name
371 | $('flags-label').alpha = 1
372 | tapped = 1
373 | } else {
374 | $('flags-label').alpha = 0
375 | tapped = 0
376 | }
377 | }
378 | })
379 | }
380 | }
381 | })
382 |
383 | // 增加 30 的距离
384 | insetMargin = insetMargin + 30
385 | })
386 | }
387 |
388 | // Power+ User 判定
389 | if (userInfo.power_plus_flag === 1) {
390 | $('avatar-container').add({
391 | type: 'image',
392 | props: {
393 | src: 'assets/power-plus.png',
394 | radius: 8
395 | },
396 | layout: function(make) {
397 | make.size.equalTo($size(16, 16))
398 | make.bottom.equalTo($('avatar-container').bottom)
399 | make.right.equalTo($('avatar-container').right)
400 | }
401 | })
402 | }
403 | }
404 |
405 | module.exports = {
406 | renderUI: renderUI
407 | }
408 |
--------------------------------------------------------------------------------