├── 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 | sspai 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 | ![](https://i.loli.net/2019/07/09/5d23e6a1e772a64161.jpg) 16 | 17 | 按照新版少数派 API,显示了少数派作者: 18 | 19 | - 用户名、头像 20 | - 关注与关注者 21 | - 获得勋章 22 | - 用户简介 23 | - 以及获得成就 24 | 25 | ### Badges 勋章墙 26 | 27 | ![](https://i.loli.net/2019/07/09/5d23e6d765fe096224.png) 28 | 29 | ### Animations 动画 30 | 31 |
32 | demo gif 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 | --------------------------------------------------------------------------------