Compare commits

..

2 Commits

Author SHA1 Message Date
云上贵猪
8169c84bf9 Merge remote-tracking branch 'origin/main' 2025-04-10 21:43:59 +08:00
云上贵猪
58f57a6027 原版提交 2025-04-10 21:43:45 +08:00
21 changed files with 1470 additions and 0 deletions

5
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,5 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/

12
.idea/drop.a-hxin.cn.iml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/drop.a-hxin.cn.iml" filepath="$PROJECT_DIR$/.idea/drop.a-hxin.cn.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

16
client/404.html Executable file
View File

@ -0,0 +1,16 @@
<html>
<style>
.btlink {
color: #20a53a;
text-decoration: none;
}
</style>
<meta charset="UTF-8">
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr>
<div style="text-align: center;font-size: 15px" >Power by <a class="btlink" href="https://www.bt.cn/?from=404" target="_blank">堡塔 (免费,高效和安全的托管控制面板)</a></div>
</body>
</html>

233
client/index.html Executable file
View File

@ -0,0 +1,233 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<!-- Web App Config -->
<title>Snapdrop</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#3367d6">
<meta name="color-scheme" content="dark light">
<meta name="apple-mobile-web-app-capable" content="no">
<meta name="apple-mobile-web-app-title" content="Snapdrop">
<!-- Descriptions -->
<meta name="description" content="Instantly share images, videos, PDFs, and links with people nearby. Peer2Peer and Open Source. No Setup, No Signup.">
<meta name="keywords" content="File, Transfer, Share, Peer2Peer">
<meta name="author" content="RobinLinus">
<meta property="og:title" content="Snapdrop">
<meta property="og:type" content="article">
<meta property="og:url" content="https://snapdrop.net/">
<meta property="og:author" content="https://facebook.com/RobinLinus">
<meta name="twitter:author" content="@RobinLinus">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:description" content="Instantly share images, videos, PDFs, and links with people nearby. Peer2Peer and Open Source. No Setup, No Signup.">
<meta name="og:description" content="Instantly share images, videos, PDFs, and links with people nearby. Peer2Peer and Open Source. No Setup, No Signup.">
<!-- Icons -->
<link rel="icon" sizes="96x96" href="images/favicon-96x96.png">
<link rel="shortcut icon" href="images/favicon-96x96.png">
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
<meta name="msapplication-TileImage" content="images/mstile-150x150.png">
<link rel="fluid-icon" type="image/png" href="images/android-chrome-192x192.png">
<meta name="twitter:image" content="https://snapdrop.net/images/twitter-stream.jpg">
<meta property="og:image" content="https://snapdrop.net/images/twitter-stream.jpg">
<!-- Resources -->
<link rel="stylesheet" type="text/css" href="styles.css">
<link rel="manifest" href="manifest.json">
</head>
<body translate="no">
<header class="row-reverse">
<a href="#about" class="icon-button" title="About Snapdrop">
<svg class="icon">
<use xlink:href="#info-outline" />
</svg>
</a>
<a href="#" id="notification" class="icon-button" title="Enable Notifications" hidden>
<svg class="icon">
<use xlink:href="#notifications" />
</svg>
</a>
<a href="#" id="install" class="icon-button" title="Install Snapdrop" hidden>
<svg class="icon">
<use xlink:href="#homescreen" />
</svg>
</a>
</header>
<!-- Peers -->
<x-peers class="center"></x-peers>
<x-no-peers>
<h2>在其他设备上打开Snapdrop以发送文件</h2>
</x-no-peers>
<x-instructions desktop="点击发送文件或右键发送消息" mobile="轻触发送文件或长按发送消息"></x-instructions>
<!-- Footer -->
<footer class="column">
<svg class="icon logo">
<use xlink:href="#wifi-tethering" />
</svg>
<div id="displayName" placeholder="在设备间传输文件的最简单方法"></div>
<div class="font-body2">您可以被此网络上的每个人发现</div>
</footer>
<!-- Receive Dialog -->
<x-dialog id="receiveDialog">
<x-background class="full center">
<x-paper shadow="2">
<h3>文件已接收</h3>
<div class="font-subheading" id="fileName">文件名</div>
<div class="font-body2" id="fileSize"></div>
<div class='preview' style="visibility: hidden;">
<img id='img-preview' src="">
</div>
<div class="row">
<label for="autoDownload" class="grow">全选所有</label>
<input type="checkbox" id="autoDownload" checked="">
</div>
<div class="row-reverse">
<a class="button" close id="download" title="Download File" autofocus>保存</a>
<button class="button" close>忽视</button>
</div>
</x-paper>
</x-background>
</x-dialog>
<!-- Send Text Dialog -->
<x-dialog id="sendTextDialog">
<form action="#">
<x-background class="full center">
<x-paper shadow="2">
<h3>发送信息</h3>
<div id="textInput" class="textarea" role="textbox" placeholder="发送消息" autocomplete="off" autofocus contenteditable></div>
<div class="row-reverse">
<button class="button" type="submit" close>发送</button>
<a class="button" close>取消</a>
</div>
</x-paper>
</x-background>
</form>
</x-dialog>
<!-- Receive Text Dialog -->
<x-dialog id="receiveTextDialog">
<x-background class="full center">
<x-paper shadow="2">
<h3>已收到消息</h3>
<div class="font-subheading" id="text"></div>
<div class="row-reverse">
<button class="button" id="copy" close autofocus>复制</button>
<button class="button" close>关闭</button>
</div>
</x-paper>
</x-background>
</x-dialog>
<!-- Toast -->
<div class="toast-container full center">
<x-toast class="row" shadow="1" id="toast">文件传输完成!</x-toast>
</div>
<!-- About Page -->
<x-about id="about" class="full center column">
<section class="center column fade-in">
<header class="row-reverse">
<a href="#" class="close icon-button">
<svg class="icon">
<use xlink:href="#close" />
</svg>
</a>
</header>
<svg class="icon logo">
<use xlink:href="#wifi-tethering" />
</svg>
<h1>Snapdrop</h1>
<div class="font-subheading">在设备间传输文件的最简单方法</div>
<div class="row">
<a class="icon-button" target="_blank" href="https://github.com/RobinLinus/snapdrop" title="Snapdrop on Github" rel="noreferrer">
<svg class="icon">
<use xlink:href="#github" />
</svg>
</a>
<!--<a class="icon-button" target="_blank" href="https://www.paypal.com/donate?hosted_button_id=FTP9DXUR7LA7Q" title="Help cover the server costs!" rel="noreferrer">-->
<!-- <svg class="icon">-->
<!-- <use xlink:href="#monetarization" />-->
<!-- </svg>-->
<!--</a>-->
<!--<a class="icon-button" target="_blank" href="https://twitter.com/intent/tweet?text=https://snapdrop.net%20by%20@robin_linus%20&" title="Tweet about Snapdrop" rel="noreferrer">-->
<!-- <svg class="icon">-->
<!-- <use xlink:href="#twitter" />-->
<!-- </svg>-->
<!--</a>-->
<!--<a class="icon-button" target="_blank" href="https://github.com/RobinLinus/snapdrop/blob/master/docs/faq.md" title="Frequently asked questions" rel="noreferrer">-->
<!-- <svg class="icon">-->
<!-- <use xlink:href="#help-outline" />-->
<!-- </svg>-->
<!--</a>-->
</div>
</section>
<x-background></x-background>
</x-about>
<!-- SVG Icon Library -->
<svg style="display: none;">
<symbol id=wifi-tethering viewBox="0 0 24 24">
<path d="M12 11c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 2c0-3.31-2.69-6-6-6s-6 2.69-6 6c0 2.22 1.21 4.15 3 5.19l1-1.74c-1.19-.7-2-1.97-2-3.45 0-2.21 1.79-4 4-4s4 1.79 4 4c0 1.48-.81 2.75-2 3.45l1 1.74c1.79-1.04 3-2.97 3-5.19zM12 3C6.48 3 2 7.48 2 13c0 3.7 2.01 6.92 4.99 8.65l1-1.73C5.61 18.53 4 15.96 4 13c0-4.42 3.58-8 8-8s8 3.58 8 8c0 2.96-1.61 5.53-4 6.92l1 1.73c2.99-1.73 5-4.95 5-8.65 0-5.52-4.48-10-10-10z"></path>
</symbol>
<symbol id=desktop-mac viewBox="0 0 24 24">
<path d="M21 2H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h7l-2 3v1h8v-1l-2-3h7c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 12H3V4h18v10z"></path>
</symbol>
<symbol id=phone-iphone viewBox="0 0 24 24">
<path d="M15.5 1h-8C6.12 1 5 2.12 5 3.5v17C5 21.88 6.12 23 7.5 23h8c1.38 0 2.5-1.12 2.5-2.5v-17C18 2.12 16.88 1 15.5 1zm-4 21c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm4.5-4H7V4h9v14z"></path>
</symbol>
<symbol id=tablet-mac viewBox="0 0 24 24">
<path d="M18.5 0h-14C3.12 0 2 1.12 2 2.5v19C2 22.88 3.12 24 4.5 24h14c1.38 0 2.5-1.12 2.5-2.5v-19C21 1.12 19.88 0 18.5 0zm-7 23c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm7.5-4H4V3h15v16z"></path>
</symbol>
<symbol id=info-outline viewBox="0 0 24 24">
<path d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z"></path>
</symbol>
<symbol id=close viewBox="0 0 24 24">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path>
</symbol>
<symbol id=help-outline viewBox="0 0 24 24">
<path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"></path>
</symbol>
<symbol id="twitter">
<path d="M23.954 4.569c-.885.389-1.83.654-2.825.775 1.014-.611 1.794-1.574 2.163-2.723-.951.555-2.005.959-3.127 1.184-.896-.959-2.173-1.559-3.591-1.559-2.717 0-4.92 2.203-4.92 4.917 0 .39.045.765.127 1.124C7.691 8.094 4.066 6.13 1.64 3.161c-.427.722-.666 1.561-.666 2.475 0 1.71.87 3.213 2.188 4.096-.807-.026-1.566-.248-2.228-.616v.061c0 2.385 1.693 4.374 3.946 4.827-.413.111-.849.171-1.296.171-.314 0-.615-.03-.916-.086.631 1.953 2.445 3.377 4.604 3.417-1.68 1.319-3.809 2.105-6.102 2.105-.39 0-.779-.023-1.17-.067 2.189 1.394 4.768 2.209 7.557 2.209 9.054 0 13.999-7.496 13.999-13.986 0-.209 0-.42-.015-.63.961-.689 1.8-1.56 2.46-2.548l-.047-.02z" />
</symbol>
<symbol id="github">
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</symbol>
<g id="notifications">
<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z" />
</g>
<symbol id="homescreen">
<path fill="none" d="M0 0h24v24H0V0z" />
<path d="M18 1.01L8 1c-1.1 0-2 .9-2 2v3h2V5h10v14H8v-1H6v3c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM10 15h2V8H5v2h3.59L3 15.59 4.41 17 10 11.41z" />
<path fill="none" d="M0 0h24v24H0V0z" />
</symbol>
<symbol id="monetarization">
<path d="M0 0h24v24H0z" fill="none" />
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.41 16.09V20h-2.67v-1.93c-1.71-.36-3.16-1.46-3.27-3.4h1.96c.1 1.05.82 1.87 2.65 1.87 1.96 0 2.4-.98 2.4-1.59 0-.83-.44-1.61-2.67-2.14-2.48-.6-4.18-1.62-4.18-3.67 0-1.72 1.39-2.84 3.11-3.21V4h2.67v1.95c1.86.45 2.79 1.86 2.85 3.39H14.3c-.05-1.11-.64-1.87-2.22-1.87-1.5 0-2.4.68-2.4 1.64 0 .84.65 1.39 2.67 1.91s4.18 1.39 4.18 3.91c-.01 1.83-1.38 2.83-3.12 3.16z" />
</symbol>
</svg>
<!-- Scripts -->
<script src="scripts/network.js"></script>
<script src="scripts/ui.js"></script>
<script src="scripts/clipboard.js" async></script>
<!-- Sounds -->
<audio id="blop" autobuffer="true">
<source src="sounds/blop.mp3" type="audio/mpeg">
<source src="sounds/blop.ogg" type="audio/ogg">
</audio>
<!-- no script -->
<noscript>
<x-noscript class="full center column">
<h1>启用 JS</h1>
<h3>Snardrep 只能使用 JavaScript</h3>
</x-noscript>
<style>
x-noscript {
background: #599cfc;
color: white;
z-index: 2;
}
a[href="#info"] {
color: white;
}
</style>
</noscript>
</body>

39
client/manifest.json Executable file
View File

@ -0,0 +1,39 @@
{
"name": "Snapdrop",
"short_name": "Snapdrop",
"icons": [{
"src": "images/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},{
"src": "images/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
},{
"src": "images/android-chrome-192x192-maskable.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},{
"src": "images/android-chrome-512x512-maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
},{
"src": "images/favicon-96x96.png",
"sizes": "96x96",
"type": "image/png"
}],
"background_color": "#efefef",
"display": "minimal-ui",
"theme_color": "#3367d6",
"share_target": {
"method":"GET",
"action": "/?share_target",
"params": {
"title": "title",
"text": "text",
"url": "url"
}
}
}

56
client/service-worker.js Executable file
View File

@ -0,0 +1,56 @@
var CACHE_NAME = 'snapdrop-cache-v2';
var urlsToCache = [
'index.html',
'./',
'styles.css',
'scripts/network.js',
'scripts/ui.js',
'scripts/clipboard.js',
'sounds/blop.mp3',
'images/favicon-96x96.png'
];
self.addEventListener('install', function(event) {
// Perform install steps
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// Cache hit - return response
if (response) {
return response;
}
return fetch(event.request);
}
)
);
});
self.addEventListener('activate', function(event) {
console.log('Updating Service Worker...')
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.filter(function(cacheName) {
// Return true if you want to remove this cache,
// but remember that caches are shared across
// the whole origin
return true
}).map(function(cacheName) {
return caches.delete(cacheName);
})
);
})
);
});

733
client/styles.css Executable file
View File

@ -0,0 +1,733 @@
/* Constants */
:root {
--icon-size: 24px;
--primary-color: #4285f4;
--peer-width: 120px;
--text-color: #333;
--bg-color: #fafafa;
--bg-color-secondary: #f1f3f4;
}
/* Layout */
html {
height: 100%;
}
html,
body {
margin: 0;
display: flex;
flex-direction: column;
width: 100%;
overflow-x: hidden;
}
body {
flex-grow: 1;
align-items: center;
justify-content: center;
overflow-y: hidden;
}
.row-reverse {
display: flex;
flex-direction: row-reverse;
}
.row {
display: flex;
flex-direction: row;
}
.column {
display: flex;
flex-direction: column;
}
.center {
display: flex;
align-items: center;
justify-content: center;
}
.grow {
flex-grow: 1;
}
.full {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
header {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 56px;
align-items: center;
padding: 16px;
box-sizing: border-box;
}
[hidden] {
display: none !important;
}
/* Typography */
body {
font-family: -apple-system, BlinkMacSystemFont, Roboto, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1 {
font-size: 34px;
font-weight: 400;
letter-spacing: -.01em;
line-height: 40px;
margin: 8px 0 0;
}
h2 {
font-size: 24px;
font-weight: 400;
letter-spacing: -.012em;
line-height: 32px;
}
h3 {
font-size: 20px;
font-weight: 500;
margin: 16px 0;
}
.font-subheading {
font-size: 16px;
font-weight: 400;
line-height: 24px;
word-break: break-all;
}
.font-body1,
body {
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
.font-body2 {
font-size: 12px;
line-height: 18px;
}
a {
text-decoration: none;
color: currentColor;
cursor: pointer;
}
/* Icons */
.icon {
width: var(--icon-size);
height: var(--icon-size);
fill: currentColor;
}
/* Shadows */
[shadow="1"] {
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.14),
0 1px 8px 0 rgba(0, 0, 0, 0.12),
0 3px 3px -2px rgba(0, 0, 0, 0.4);
}
[shadow="2"] {
box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14),
0 1px 10px 0 rgba(0, 0, 0, 0.12),
0 2px 4px -1px rgba(0, 0, 0, 0.4);
}
/* Animations */
@keyframes fade-in {
0% {
opacity: 0;
}
}
/* Main Header */
body>header a {
margin-left: 8px;
}
/* Peers List */
x-peers {
width: 100%;
overflow: hidden;
flex-flow: row wrap;
z-index: 2;
}
/* Empty Peers List */
x-no-peers {
padding: 8px;
text-align: center;
/* prevent flickering on load */
animation: fade-in 300ms;
animation-delay: 500ms;
animation-fill-mode: backwards;
}
x-no-peers h2,
x-no-peers a {
color: var(--primary-color);
}
x-peers:not(:empty)+x-no-peers {
display: none;
}
/* Peer */
x-peer {
-webkit-user-select: none;
user-select: none;
}
x-peer label {
width: var(--peer-width);
padding: 8px;
cursor: pointer;
touch-action: manipulation;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
position: relative;
}
x-peer .name {
width: var(--peer-width);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
input[type="file"] {
visibility: hidden;
position: absolute;
}
x-peer x-icon {
--icon-size: 40px;
width: var(--icon-size);
padding: 12px;
border-radius: 50%;
background: var(--primary-color);
color: white;
display: flex;
margin-bottom: 8px;
transition: transform 150ms;
will-change: transform;
}
x-peer:not([transfer]):hover x-icon,
x-peer:not([transfer]):focus x-icon {
transform: scale(1.05);
}
x-peer[transfer] x-icon {
box-shadow: none;
opacity: 0.8;
transform: scale(1);
}
.status,
.device-name {
height: 18px;
opacity: 0.7;
}
x-peer[transfer] .status:before {
content: 'Transferring...';
}
x-peer:not([transfer]) .status,
x-peer[transfer] .device-name {
display: none;
}
x-peer x-icon {
animation: pop 600ms ease-out 1;
}
@keyframes pop {
0% {
transform: scale(0.7);
}
40% {
transform: scale(1.2);
}
}
x-peer[drop] x-icon {
transform: scale(1.1);
}
/* Footer */
footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
align-items: center;
padding: 0 0 16px 0;
text-align: center;
}
footer .logo {
--icon-size: 80px;
margin-bottom: 8px;
color: var(--primary-color);
}
footer .font-body2 {
color: var(--primary-color);
}
@media (min-height: 800px) {
footer {
margin-bottom: 16px;
}
}
/* Dialog */
x-dialog x-background {
background: rgba(0, 0, 0, 0.61);
z-index: 10;
transition: opacity 300ms;
will-change: opacity;
padding: 16px;
}
x-dialog x-paper {
z-index: 3;
background: white;
border-radius: 8px;
padding: 16px 24px;
width: 100%;
max-width: 400px;
box-sizing: border-box;
transition: transform 300ms;
will-change: transform;
}
x-dialog:not([show]) {
pointer-events: none;
}
x-dialog:not([show]) x-paper {
transform: scale(0.1);
}
x-dialog:not([show]) x-background {
opacity: 0;
}
x-dialog .row-reverse>.button {
margin-top: 16px;
margin-left: 8px;
}
x-dialog a {
color: var(--primary-color);
}
/* Receive Dialog */
#receiveDialog .row {
margin-top: 24px;
margin-bottom: 8px;
}
/* Receive Text Dialog */
#receiveTextDialog #text {
width: 100%;
word-break: break-all;
max-height: 300px;
overflow-x: hidden;
overflow-y: auto;
-webkit-user-select: all;
-moz-user-select: all;
user-select: all;
white-space: pre-wrap;
}
#receiveTextDialog #text a {
cursor: pointer;
}
#receiveTextDialog #text a:hover {
text-decoration: underline;
}
#receiveTextDialog h3 {
/* Select the received text when double-clicking the dialog */
user-select: none;
pointer-events: none;
}
/* Button */
.button {
padding: 0 16px;
box-sizing: border-box;
min-height: 36px;
min-width: 100px;
font-size: 14px;
line-height: 24px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
white-space: nowrap;
cursor: pointer;
user-select: none;
background: inherit;
color: var(--primary-color);
}
.button,
.icon-button {
position: relative;
display: flex;
align-items: center;
justify-content: center;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
touch-action: manipulation;
border: none;
outline: none;
}
.button:before,
.icon-button:before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: currentColor;
opacity: 0;
transition: opacity 300ms;
}
.button:hover:before,
.icon-button:hover:before {
opacity: 0.1;
}
.button:before {
border-radius: 8px;
}
.button:focus:before,
.icon-button:focus:before {
opacity: 0.2;
}
button::-moz-focus-inner {
border: 0;
}
/* Icon Button */
.icon-button {
width: 40px;
height: 40px;
}
.icon-button:before {
border-radius: 50%;
}
/* Text Input */
.textarea {
box-sizing: border-box;
border: none;
outline: none;
padding: 16px 24px;
border-radius: 16px;
margin: 8px 0;
font-size: 14px;
font-family: inherit;
background: #f1f3f4;
display: block;
overflow: auto;
resize: none;
min-height: 40px;
line-height: 16px;
max-height: 300px;
}
/* Info Animation */
#about {
color: white;
z-index: 11;
overflow: hidden;
pointer-events: none;
text-align: center;
}
#about .fade-in {
transition: opacity 300ms;
will-change: opacity;
transition-delay: 300ms;
z-index: 11;
pointer-events: all;
}
#about:not(:target) .fade-in {
opacity: 0;
pointer-events: none;
transition-delay: 0;
}
#about .logo {
--icon-size: 96px;
}
#about x-background {
position: absolute;
top: calc(32px - 200px);
right: calc(32px - 200px);
width: 400px;
height: 400px;
border-radius: 50%;
background: var(--primary-color);
transform: scale(0);
z-index: -1;
}
/* Hack such that initial scale(0) isn't animated */
#about x-background {
will-change: transform;
transition: transform 800ms cubic-bezier(0.77, 0, 0.175, 1);
}
#about:target x-background {
transform: scale(12);
}
#about .row a {
margin: 8px 8px -16px;
}
/* Loading Indicator */
.progress {
width: 80px;
height: 80px;
position: absolute;
top: 0px;
clip: rect(0px, 80px, 80px, 40px);
--progress: rotate(0deg);
transition: transform 200ms;
}
.circle {
width: 72px;
height: 72px;
border: 4px solid var(--primary-color);
border-radius: 40px;
position: absolute;
clip: rect(0px, 40px, 80px, 0px);
will-change: transform;
transform: var(--progress);
}
.over50 {
clip: rect(auto, auto, auto, auto);
}
.over50 .circle.right {
transform: rotate(180deg);
}
/* Generic placeholder */
[placeholder]:empty:before {
content: attr(placeholder);
}
/* Toast */
.toast-container {
padding: 0 8px 24px;
overflow: hidden;
pointer-events: none;
}
x-toast {
position: absolute;
min-height: 48px;
bottom: 24px;
width: 100%;
max-width: 344px;
background-color: #323232;
color: rgba(255, 255, 255, 0.95);
align-items: center;
box-sizing: border-box;
padding: 8px 24px;
z-index: 20;
transition: opacity 200ms, transform 300ms ease-out;
cursor: default;
line-height: 24px;
border-radius: 8px;
pointer-events: all;
}
x-toast:not([show]):not(:hover) {
opacity: 0;
transform: translateY(100px);
}
/* Instructions */
x-instructions {
position: absolute;
top: 120px;
opacity: 0.5;
transition: opacity 300ms;
z-index: -1;
text-align: center;
}
x-instructions:before {
content: attr(mobile);
}
x-peers:empty~x-instructions {
opacity: 0;
}
/* Responsive Styles */
@media (min-height: 800px) {
footer {
margin-bottom: 16px;
}
}
@media screen and (min-height: 800px),
screen and (min-width: 1100px) {
x-instructions:before {
content: attr(desktop);
}
}
@media (max-height: 420px) {
x-instructions {
top: 24px;
}
footer .logo {
--icon-size: 40px;
}
}
/*
iOS specific styles
*/
@supports (-webkit-overflow-scrolling: touch) {
html {
position: fixed;
}
x-instructions:before {
content: attr(mobile);
}
}
/*
Color Themes
*/
/* Default colors */
body {
--text-color: #333;
--bg-color: #fafafa;
--bg-color-secondary: #f1f3f4;
}
/* Colored Elements */
body {
color: var(--text-color);
background-color: var(--bg-color);
transition: background-color 0.5s ease;
}
x-dialog x-paper {
background-color: var(--bg-color);
}
.textarea {
color: var(--text-color);
background-color: var(--bg-color-secondary);
}
/* Image Preview */
#img-preview{
max-width: 100%;
max-height: 50vh;
margin: auto;
display: block;
}
/*
Edge specific styles
*/
@supports (-ms-ime-align: auto) {
html,
body {
overflow: hidden;
}
}

25
docker-compose.yml Executable file
View File

@ -0,0 +1,25 @@
version: "3"
services:
node:
image: "node:lts-alpine"
user: "node"
working_dir: /home/node/app
volumes:
- ./server/:/home/node/app
command: ash -c "npm i && node index.js"
nginx:
build:
context: ./docker/
dockerfile: nginx-with-openssl.Dockerfile
image: "nginx-with-openssl"
volumes:
- ./client:/usr/share/nginx/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
- ./docker/certs:/etc/ssl/certs
- ./docker/openssl:/mnt/openssl
ports:
- "8080:80"
- "443:443"
env_file: ./docker/fqdn.env
entrypoint: /mnt/openssl/create.sh
command: ["nginx", "-g", "daemon off;"]

1
docker/fqdn.env Executable file
View File

@ -0,0 +1 @@
FQDN=localhost

View File

@ -0,0 +1,3 @@
FROM nginx:alpine
RUN apk add --no-cache openssl

75
docker/nginx/default.conf Executable file
View File

@ -0,0 +1,75 @@
server {
listen 80;
#server_name your.domain;
#charset koi8-r;
#access_log /var/log/nginx/host.access.log main;
expires epoch;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
location /server {
proxy_connect_timeout 300;
proxy_pass http://node:3000;
proxy_set_header Connection "upgrade";
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Forwarded-for $remote_addr;
}
location /ca.crt {
alias /etc/ssl/certs/snapdropCA.crt;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
server {
listen 443 ssl http2;
ssl_certificate /etc/ssl/certs/snapdrop-dev.crt;
ssl_certificate_key /etc/ssl/certs/snapdrop-dev.key;
#server_name ;
#charset koi8-r;
#access_log /var/log/nginx/host.access.log main;
expires epoch;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
location /server {
proxy_connect_timeout 300;
proxy_pass http://node:3000;
proxy_set_header Connection "upgrade";
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Forwarded-for $remote_addr;
}
location /ca.crt {
alias /etc/ssl/certs/snapdropCA.crt;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

9
docker/openssl/create.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/sh
cnf_dir='/mnt/openssl/'
certs_dir='/etc/ssl/certs/'
openssl req -config ${cnf_dir}snapdropCA.cnf -new -x509 -days 1 -keyout ${certs_dir}snapdropCA.key -out ${certs_dir}snapdropCA.crt
openssl req -config ${cnf_dir}snapdropCert.cnf -new -out /tmp/snapdrop-dev.csr -keyout ${certs_dir}snapdrop-dev.key
openssl x509 -req -in /tmp/snapdrop-dev.csr -CA ${certs_dir}snapdropCA.crt -CAkey ${certs_dir}snapdropCA.key -CAcreateserial -extensions req_ext -extfile ${cnf_dir}snapdropCert.cnf -sha512 -days 1 -out ${certs_dir}snapdrop-dev.crt
exec "$@"

26
docker/openssl/snapdropCA.cnf Executable file
View File

@ -0,0 +1,26 @@
[ req ]
default_bits = 2048
default_md = sha256
default_days = 1
encrypt_key = no
distinguished_name = subject
x509_extensions = x509_ext
string_mask = utf8only
prompt = no
[ subject ]
organizationName = Snapdrop
OU = CA
commonName = snapdrop-CA
[ x509_ext ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
# You only need digitalSignature below. *If* you don't allow
# RSA Key transport (i.e., you use ephemeral cipher suites), then
# omit keyEncipherment because that's key transport.
basicConstraints = critical, CA:TRUE, pathlen:0
keyUsage = critical, digitalSignature, keyEncipherment, cRLSign, keyCertSign

29
docker/openssl/snapdropCert.cnf Executable file
View File

@ -0,0 +1,29 @@
[ req ]
default_bits = 2048
default_md = sha256
default_days = 1
encrypt_key = no
distinguished_name = subject
req_extensions = req_ext
string_mask = utf8only
prompt = no
[ subject ]
organizationName = Snapdrop
OU = Development
# Use a friendly name here because it's presented to the user. The server's DNS
# names are placed in Subject Alternate Names. Plus, DNS names here is deprecated
# by both IETF and CA/Browser Forums. If you place a DNS name here, then you
# must include the DNS name in the SAN too (otherwise, Chrome and others that
# strictly follow the CA/Browser Baseline Requirements will fail).
commonName = ${ENV::FQDN}
[ req_ext ]
subjectKeyIdentifier = hash
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
subjectAltName = DNS:${ENV::FQDN}
nsComment = "OpenSSL Generated Certificate"
extendedKeyUsage = serverAuth

76
docs/faq.md Executable file
View File

@ -0,0 +1,76 @@
# Frequently Asked Questions
### Instructions / Discussions
* [Video Instructions](https://www.youtube.com/watch?v=4XN02GkcHUM) (Big thanks to [TheiTeckHq](https://www.youtube.com/channel/UC_DUzWMb8gZZnAbISQjmAfQ))
* [idownloadblog](http://www.idownloadblog.com/2015/12/29/snapdrop/)
* [thenextweb](http://thenextweb.com/insider/2015/12/27/snapdrop-is-a-handy-web-based-replacement-for-apples-fiddly-airdrop-file-transfer-tool/)
* [winboard](http://www.winboard.org/artikel-ratgeber/6253-dateien-vom-desktop-pc-mit-anderen-plattformen-teilen-mit-snapdrop.html)
* [免費資源網路社群](https://free.com.tw/snapdrop/)
* [Hackernews](https://news.ycombinator.com/front?day=2020-12-24)
* [Reddit](https://www.reddit.com/r/Android/comments/et4qny/snapdrop_is_a_free_open_source_cross_platform/)
* [Producthunt](https://www.producthunt.com/posts/snapdrop)
### Help! I can't install the PWA!
if you are using a Chromium-based browser (Chrome, Edge, Brave, etc.), you can easily install Snapdrop PWA on your desktop by clicking the install button in the top-right corner while on [snapdrop.net](https://snapdrop.net) (see below).
<img src="pwa-install.png">
### What about the connection? Is it a P2P-connection directly from device to device or is there any third-party-server?
It uses a P2P connection if WebRTC is supported by the browser. WebRTC needs a Signaling Server, but it is only used to establish a connection and is not involved in the file transfer.
### What about privacy? Will files be saved on third-party-servers?
None of your files are ever sent to any server. Files are sent only between peers. Snapdrop doesn't even use a database. If you are curious have a look [at the Server](https://github.com/RobinLinus/snapdrop/blob/master/server/). Even if Snapdrop was able to view the files being transfered, WebRTC encrypts the files on transit, so the server would be unable to read them.
### What about security? Are my files encrypted while being sent between the computers?
Yes. Your files are sent using WebRTC, which encrypts them on transit.
### Why don't you implement feature xyz?
Snapdrop is a study in radical simplicity. The user interface is insanely simple. Features are chosen very carefully because complexity grows quadratically since every feature potentially interferes with each other feature. We focus very narrowly on a single use case: instant file transfer.
We are not trying to optimize for some edge-cases. We are optimizing the user flow of the average users. Don't be sad if we decline your feature request for the sake of simplicity.
If you want to learn more about simplicity you can read [Insanely Simple: The Obsession that Drives Apple's Success](https://www.amazon.com/Insanely-Simple-Ken-Segall-audiobook/dp/B007Z9686O) or [Thinking, Fast and Slow](https://www.amazon.com/Thinking-Fast-Slow-Daniel-Kahneman/dp/0374533555).
### Snapdrop is awesome! How can I support it?
* [Donate via PayPal to help cover the server costs](https://www.paypal.com/donate/?hosted_button_id=FTP9DXUR7LA7Q)
* [File bugs, give feedback, submit suggestions](https://github.com/RobinLinus/snapdrop/issues)
* Share Snapdrop on your social media.
* Fix bugs and make a pull request.
* Do security analysis and suggestions
## "Inofficial" Instances
Here's a list of other people hosting inofficial instances of Snapdrop:
- https://pairdrop.net/
- https://snapdrop.k26.ch/
- https://snapdrop.9pfs.repl.co/
- https://filedrop.codext.de/
- https://s.hoothin.com/
- https://www.wulingate.com/
- https://snapdrop.fairysoft.net/
- https://airtransferer.web.app/
- https://drop.wuyuan.dev
- https://share.jck.cx
DISCLAIMER: WE ARE NOT IN ANY WAY AFFILIATED WITH THE PEOPLE WHO RUN THESE INSTANCES. WE DO NOT KNOW THEM. WE CANNOT VERIFY THE CODE THEY ARE RUNNING!
## Third-Party Apps
Here's a list of some third-party Snapdrop apps:
1. [Snapdrop Desktop App](https://github.com/alextwothousand/snapdrop-desktop) built on top of Electron. (Thanks to [alextwothousand!](https://github.com/alextwothousand/)).
1. [Snapdrop Android App](https://github.com/fm-sys/snapdrop-android) allows you to also send files directly from other apps via the share action.
1. [Snapdrop Flutter App](https://github.com/congnguyendinh0/snapdrop_flutter)
1. [Snapdrop iOS App](https://github.com/CDsigma/Snapdrop-iOS-App)
1. [Snapdrop Node App (with completely Node server)](https://github.com/Bellisario/node-snapdrop)
1. [SnapDrop VSCode Extension](https://github.com/Yash-Garg/snapdrop-vsc)
1. Feel free to make one :)
[< Back](/README.md)

61
docs/local-dev.md Executable file
View File

@ -0,0 +1,61 @@
# Local Development
## Install
First, [Install docker with docker-compose.](https://docs.docker.com/compose/install/)
Then, clone the repository:
```
git clone https://github.com/RobinLinus/snapdrop.git
cd snapdrop
docker-compose up -d
```
Now point your browser to `http://localhost:8080`.
- To restart the containers run `docker-compose restart`.
- To stop the containers run `docker-compose stop`.
- To debug the NodeJS server run `docker logs snapdrop_node_1`.
## Run locally by pulling image from Docker Hub
Have docker installed, then use the command:
```
docker pull linuxserver/snapdrop
```
To run the image, type (if port 8080 is occupied by host use another random port <random port>:80):
```
docker run -d -p 8080:80 linuxserver/snapdrop
```
## Testing PWA related features
PWAs require that the app is served under a correctly set up and trusted TLS endpoint.
The nginx container creates a CA certificate and a website certificate for you. To correctly set the common name of the certificate, you need to change the FQDN environment variable in `docker/fqdn.env` to the fully qualified domain name of your workstation.
If you want to test PWA features, you need to trust the CA of the certificate for your local deployment. For your convenience, you can download the crt file from `http://<Your FQDN>:8080/ca.crt`. Install that certificate to the trust store of your operating system.
- On Windows, make sure to install it to the `Trusted Root Certification Authorities` store.
- On MacOS, double click the installed CA certificate in `Keychain Access`, expand `Trust`, and select `Always Trust` for SSL.
- Firefox uses its own trust store. To install the CA, point Firefox at `http://<Your FQDN>:8080/ca.crt`. When prompted, select `Trust this CA to identify websites` and click OK.
- When using Chrome, you need to restart Chrome so it reloads the trust store (`chrome://restart`). Additionally, after installing a new cert, you need to clear the Storage (DevTools -> Application -> Clear storage -> Clear site data).
Please note that the certificates (CA and webserver cert) expire after a day.
Also, whenever you restart the nginx docker, container new certificates are created.
The site is served on `https://<Your FQDN>:443`.
   
## Deployment Notes
The client expects the server at http(s)://your.domain/server.
When serving the node server behind a proxy, the `X-Forwarded-For` header has to be set by the proxy. Otherwise, all clients that are served by the proxy will be mutually visible.
By default, the server listens on port 3000.
For an nginx configuration example, see `docker/nginx/default.conf`.
[< Back](/README.md)

BIN
docs/pwa-install.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

16
server/package.json Executable file
View File

@ -0,0 +1,16 @@
{
"name": "snapdrop",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"ua-parser-js": "^0.7.24",
"unique-names-generator": "^4.3.0",
"ws": "^7.4.6"
}
}

41
server/pnpm-lock.yaml generated Normal file
View File

@ -0,0 +1,41 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
ua-parser-js:
specifier: ^0.7.24
version: 0.7.40
unique-names-generator:
specifier: ^4.3.0
version: 4.7.1
ws:
specifier: ^7.4.6
version: 7.5.10
packages:
/ua-parser-js@0.7.40:
resolution: {integrity: sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ==}
hasBin: true
dev: false
/unique-names-generator@4.7.1:
resolution: {integrity: sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==}
engines: {node: '>=8'}
dev: false
/ws@7.5.10:
resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==}
engines: {node: '>=8.3.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ^5.0.2
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
dev: false