@lppx/lanshare 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # LanShare - 局域网文件分享工具
2
+
3
+ 一个简单易用的局域网文件分享工具,支持通过浏览器上传和下载文件。
4
+
5
+ ## 功能特性
6
+
7
+ - 🚀 交互式命令行界面
8
+ - 🌐 选择网卡和 IP 地址
9
+ - 🔧 自定义端口号
10
+ - 📤 文件上传
11
+ - 📥 文件下载
12
+ - 🎨 简洁的 Web 界面
13
+ - 📝 完整的日志记录
14
+
15
+ ## 安装
16
+
17
+ ```bash
18
+ npm install -g @lppx/lanshare
19
+ ```
20
+
21
+ ## 使用方法
22
+
23
+ ### 启动服务器
24
+
25
+ ```bash
26
+ lanshare
27
+ # 或
28
+ lsh
29
+ ```
30
+
31
+ ### 交互式配置
32
+
33
+ 启动后会依次提示:
34
+
35
+ 1. 选择网卡
36
+ 2. 输入 IP 地址(默认为选中网卡的 IP)
37
+ 3. 输入端口号(默认 3000)
38
+
39
+ ### 访问服务
40
+
41
+ 启动成功后,在浏览器中访问显示的地址,例如:
42
+
43
+ ```
44
+ http://192.168.1.100:3000
45
+ ```
46
+
47
+ 局域网内的其他设备也可以通过该地址访问。
48
+
49
+ ## 日志功能
50
+
51
+ 应用会自动记录运行日志,方便调试和问题排查。
52
+
53
+ ### 日志位置
54
+
55
+ 日志文件保存在用户主目录下:
56
+
57
+ ```
58
+ ~/.lanshare/logs/
59
+ ├── lanshare.log # 所有日志
60
+ └── error.log # 仅错误日志
61
+ ```
62
+
63
+ ### 日志级别
64
+
65
+ 默认日志级别为 `info`,可通过环境变量 `LOG_LEVEL` 调整:
66
+
67
+ ```bash
68
+ # 设置为 debug 级别查看更详细的日志
69
+ LOG_LEVEL=debug lanshare
70
+
71
+ # 可选级别: error, warn, info, debug
72
+ ```
73
+
74
+ ### 日志内容
75
+
76
+ 日志会记录以下信息:
77
+
78
+ - 服务器启动/停止
79
+ - HTTP 请求(IP、方法、路径)
80
+ - 文件上传(文件名、大小)
81
+ - 文件下载
82
+ - 错误信息和堆栈跟踪
83
+
84
+ ### 日志轮转
85
+
86
+ - 单个日志文件最大 5MB
87
+ - 最多保留 5 个历史文件
88
+ - 超出限制会自动清理旧日志
89
+
90
+ ## 开发
91
+
92
+ ### 安装依赖
93
+
94
+ ```bash
95
+ npm install
96
+ ```
97
+
98
+ ### 构建
99
+
100
+ ```bash
101
+ npm run build
102
+ ```
103
+
104
+ ### 本地测试
105
+
106
+ ```bash
107
+ npm link
108
+ lanshare
109
+ ```
110
+
111
+ ## 技术栈
112
+
113
+ - TypeScript
114
+ - Express
115
+ - Commander
116
+ - Prompts
117
+ - Multer
118
+
119
+ ## 许可证
120
+
121
+ MIT
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env ts-node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const child_process_1 = require("child_process");
5
+ const fs_1 = require("fs");
6
+ const path_1 = require("path");
7
+ // #region 颜色定义
8
+ const colors = {
9
+ red: '\x1b[0;31m',
10
+ green: '\x1b[0;32m',
11
+ yellow: '\x1b[1;33m',
12
+ reset: '\x1b[0m',
13
+ };
14
+ // #endregion
15
+ // #region 辅助函数
16
+ function printSuccess(message) {
17
+ console.log(`${colors.green}✓ ${message}${colors.reset}`);
18
+ }
19
+ function printError(message) {
20
+ console.error(`${colors.red}✗ ${message}${colors.reset}`);
21
+ }
22
+ function printWarning(message) {
23
+ console.log(`${colors.yellow}⚠ ${message}${colors.reset}`);
24
+ }
25
+ // #endregion
26
+ // #region Git 操作
27
+ function execGit(command) {
28
+ try {
29
+ return (0, child_process_1.execSync)(`git ${command}`, {
30
+ encoding: 'utf-8',
31
+ stdio: ['pipe', 'pipe', 'pipe']
32
+ }).trim();
33
+ }
34
+ catch (error) {
35
+ throw new Error(error.stderr || error.message);
36
+ }
37
+ }
38
+ async function syncRepo() {
39
+ const dir = process.cwd();
40
+ console.log('开始同步仓库...');
41
+ // 检查是否在 git 仓库中
42
+ if (!(0, fs_1.existsSync)((0, path_1.join)(dir, '.git'))) {
43
+ printError('当前目录不是 git 仓库');
44
+ process.exit(1);
45
+ }
46
+ // 执行 git pull
47
+ console.log('正在拉取远程更改...');
48
+ try {
49
+ execGit('pull');
50
+ printSuccess('成功拉取远程更改');
51
+ }
52
+ catch (error) {
53
+ printError('拉取失败,可能存在冲突');
54
+ printWarning('请手动解决冲突后再运行此脚本');
55
+ console.error(error.message);
56
+ process.exit(1);
57
+ }
58
+ // 检查是否有更改需要提交
59
+ const status = execGit('status --porcelain');
60
+ if (!status) {
61
+ printWarning('没有需要提交的更改');
62
+ process.exit(0);
63
+ }
64
+ // 添加所有更改
65
+ console.log('正在添加所有更改...');
66
+ execGit('add -A');
67
+ printSuccess('已添加所有更改');
68
+ // 生成 commit message(当前日期和时间)
69
+ const now = new Date();
70
+ const commitMsg = now.toLocaleString('zh-CN', {
71
+ year: 'numeric',
72
+ month: '2-digit',
73
+ day: '2-digit',
74
+ hour: '2-digit',
75
+ minute: '2-digit',
76
+ second: '2-digit',
77
+ hour12: false,
78
+ }).replace(/\//g, '-');
79
+ console.log('正在提交更改...');
80
+ execGit(`commit -m "${commitMsg}"`);
81
+ printSuccess(`提交成功: ${commitMsg}`);
82
+ // 推送到远程仓库
83
+ console.log('正在推送到远程仓库...');
84
+ try {
85
+ execGit('push');
86
+ printSuccess('成功推送到远程仓库');
87
+ }
88
+ catch (error) {
89
+ printError('推送失败');
90
+ console.error(error.message);
91
+ process.exit(1);
92
+ }
93
+ printSuccess('仓库同步完成!');
94
+ }
95
+ // #endregion
96
+ // 执行主函数
97
+ syncRepo().catch((error) => {
98
+ printError(`发生错误: ${error.message}`);
99
+ process.exit(1);
100
+ });
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createServer = createServer;
7
+ const express_1 = __importDefault(require("express"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const os_1 = __importDefault(require("os"));
11
+ const http_1 = require("http");
12
+ const upload_1 = require("./routers/upload");
13
+ const files_1 = require("./routers/files");
14
+ const logger_1 = __importDefault(require("./utils/logger"));
15
+ const socket_1 = require("./socket");
16
+ function createServer(config) {
17
+ const app = (0, express_1.default)();
18
+ const httpServer = (0, http_1.createServer)(app);
19
+ const { io, connectedDevices } = (0, socket_1.setupSocketIO)(httpServer); // 初始化 Socket.IO
20
+ const uploadDir = config.uploadDir || path_1.default.join(os_1.default.homedir(), 'lanshare-uploads');
21
+ // #region 确保上传目录存在
22
+ if (!fs_1.default.existsSync(uploadDir)) {
23
+ fs_1.default.mkdirSync(uploadDir, { recursive: true });
24
+ logger_1.default.info(`创建上传目录: ${uploadDir}`);
25
+ }
26
+ // #endregion
27
+ // #region 中间件
28
+ app.use(express_1.default.json());
29
+ app.use(express_1.default.urlencoded({ extended: true }));
30
+ app.use(express_1.default.static(path_1.default.join(__dirname, '../../public')));
31
+ // 请求日志中间件
32
+ app.use((req, _res, next) => {
33
+ logger_1.default.info(`${req.method} ${req.path} - ${req.ip}`);
34
+ next();
35
+ });
36
+ // #endregion
37
+ // #region 路由
38
+ app.use((0, upload_1.createUploadRouter)(uploadDir));
39
+ app.use((0, files_1.createFilesRouter)(uploadDir));
40
+ // 获取已连接设备列表
41
+ app.get('/api/devices', (_req, res) => {
42
+ res.json(Array.from(connectedDevices.values()));
43
+ });
44
+ // #endregion
45
+ return { app, httpServer, io, uploadDir, connectedDevices };
46
+ }
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.handleStartCommand = handleStartCommand;
40
+ const prompts_1 = __importDefault(require("prompts"));
41
+ const index_1 = require("../index");
42
+ const logger_1 = __importStar(require("../utils/logger"));
43
+ const network_1 = require("./network");
44
+ /**
45
+ * 处理 start 命令
46
+ */
47
+ async function handleStartCommand() {
48
+ try {
49
+ logger_1.default.info('启动 lanshare 服务');
50
+ const networkInterfaces = (0, network_1.getNetworkInterfaces)();
51
+ if (networkInterfaces.length === 0) {
52
+ logger_1.default.error('未找到可用的网络接口');
53
+ process.exit(1);
54
+ }
55
+ // #region 选择网卡
56
+ const interfaceChoices = networkInterfaces.map(iface => ({
57
+ title: `${iface.name} (${iface.address})`,
58
+ value: iface
59
+ }));
60
+ const interfaceResponse = await (0, prompts_1.default)({
61
+ type: 'select',
62
+ name: 'interface',
63
+ message: '请选择网卡',
64
+ choices: interfaceChoices,
65
+ initial: 0
66
+ });
67
+ if (!interfaceResponse.interface) {
68
+ logger_1.default.info('用户取消操作');
69
+ process.exit(0);
70
+ }
71
+ const selectedInterface = interfaceResponse.interface;
72
+ // #endregion
73
+ // #region 输入 IP 地址
74
+ const ipResponse = await (0, prompts_1.default)({
75
+ type: 'text',
76
+ name: 'ip',
77
+ message: '请输入 IP 地址',
78
+ initial: selectedInterface.address,
79
+ validate: (value) => {
80
+ const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
81
+ if (!ipRegex.test(value)) {
82
+ return 'IP 地址格式不正确';
83
+ }
84
+ const parts = value.split('.');
85
+ if (parts.some((part) => parseInt(part) > 255)) {
86
+ return 'IP 地址范围不正确';
87
+ }
88
+ return true;
89
+ }
90
+ });
91
+ if (!ipResponse.ip) {
92
+ logger_1.default.info('用户取消操作');
93
+ process.exit(0);
94
+ }
95
+ // #endregion
96
+ // #region 输入端口号
97
+ const portResponse = await (0, prompts_1.default)({
98
+ type: 'text',
99
+ name: 'port',
100
+ message: '请输入端口号',
101
+ initial: '3001',
102
+ validate: (value) => {
103
+ if (!value || value.trim() === '') {
104
+ return '端口号不能为空';
105
+ }
106
+ const port = Number(value);
107
+ if (isNaN(port) || port < 1 || port > 65535) {
108
+ return '端口号必须在 1-65535 之间';
109
+ }
110
+ return true;
111
+ }
112
+ });
113
+ if (!portResponse.port) {
114
+ logger_1.default.info('用户取消操作');
115
+ process.exit(0);
116
+ }
117
+ // #endregion
118
+ const port = Number(portResponse.port);
119
+ logger_1.default.info(`配置完成 - IP: ${ipResponse.ip}, Port: ${port}`);
120
+ // 启动服务器
121
+ logger_1.default.info(`日志目录: ${logger_1.logDir}`);
122
+ await (0, index_1.startServer)({
123
+ ip: ipResponse.ip,
124
+ port: port
125
+ });
126
+ }
127
+ catch (error) {
128
+ logger_1.default.error('启动失败', error);
129
+ process.exit(1);
130
+ }
131
+ }
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.createProgram = createProgram;
5
+ const commander_1 = require("commander");
6
+ const cli_1 = require("./cli");
7
+ /**
8
+ * 创建并配置命令行程序
9
+ */
10
+ function createProgram() {
11
+ const program = new commander_1.Command();
12
+ program
13
+ .name('lanshare')
14
+ .description('局域网文件分享工具')
15
+ .version('1.0.0');
16
+ program
17
+ .command('start')
18
+ .description('启动文件分享服务器')
19
+ .action(cli_1.handleStartCommand);
20
+ return program;
21
+ }
22
+ async function main() {
23
+ const program = createProgram();
24
+ // 默认执行 start 命令
25
+ if (process.argv.length === 2) {
26
+ process.argv.push('start');
27
+ }
28
+ program.parse();
29
+ }
30
+ main();
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getNetworkInterfaces = getNetworkInterfaces;
7
+ const os_1 = __importDefault(require("os"));
8
+ /**
9
+ * 获取所有可用的 IPv4 网络接口
10
+ */
11
+ function getNetworkInterfaces() {
12
+ const interfaces = os_1.default.networkInterfaces();
13
+ const result = [];
14
+ for (const [name, addresses] of Object.entries(interfaces)) {
15
+ if (!addresses)
16
+ continue;
17
+ for (const addr of addresses) {
18
+ if (addr.family === 'IPv4' && !addr.internal) {
19
+ result.push({
20
+ name,
21
+ address: addr.address,
22
+ family: addr.family,
23
+ internal: addr.internal
24
+ });
25
+ }
26
+ }
27
+ }
28
+ return result;
29
+ }
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
16
+ };
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ // 此文件已废弃,请使用 src/cli/index.ts
19
+ __exportStar(require("./cli/index"), exports);
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.startServer = startServer;
7
+ const app_1 = require("./app");
8
+ const logger_1 = __importDefault(require("./utils/logger"));
9
+ function startServer(config) {
10
+ const { httpServer, uploadDir } = (0, app_1.createServer)(config);
11
+ return new Promise((resolve, reject) => {
12
+ try {
13
+ httpServer.listen(config.port, config.ip, () => {
14
+ logger_1.default.info(`服务器启动成功✅ - http://${config.ip}:${config.port}`);
15
+ logger_1.default.info(`上传目录: ${uploadDir}`);
16
+ logger_1.default.info('Socket.IO 已启用,等待设备连接...');
17
+ logger_1.default.info('按 Ctrl+C 停止服务器');
18
+ resolve();
19
+ });
20
+ }
21
+ catch (error) {
22
+ logger_1.default.error('服务器启动失败', error);
23
+ reject(error);
24
+ }
25
+ });
26
+ }
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createFilesRouter = createFilesRouter;
7
+ const express_1 = require("express");
8
+ const path_1 = __importDefault(require("path"));
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const os_1 = __importDefault(require("os"));
11
+ const logger_1 = __importDefault(require("../utils/logger"));
12
+ // 存储文件下载次数
13
+ const downloadCounts = {};
14
+ // 存储已连接设备
15
+ const connectedDevices = new Map();
16
+ function createFilesRouter(uploadDir) {
17
+ const router = (0, express_1.Router)();
18
+ // #region 获取文件列表
19
+ router.get('/files', (_req, res) => {
20
+ fs_1.default.readdir(uploadDir, (err, files) => {
21
+ if (err) {
22
+ logger_1.default.error('读取文件列表失败', err);
23
+ return res.status(500).json({ error: '读取文件列表失败' });
24
+ }
25
+ const fileDetails = files.map(filename => {
26
+ const filepath = path_1.default.join(uploadDir, filename);
27
+ const stats = fs_1.default.statSync(filepath);
28
+ return {
29
+ name: filename,
30
+ size: stats.size,
31
+ downloads: downloadCounts[filename] || 0,
32
+ sharedBy: os_1.default.hostname(),
33
+ ip: getLocalIP(),
34
+ createdAt: stats.birthtime
35
+ };
36
+ });
37
+ logger_1.default.debug(`获取文件列表: ${files.length} 个文件`);
38
+ res.json(fileDetails);
39
+ });
40
+ });
41
+ // #endregion
42
+ // #region 下载文件
43
+ router.get('/download/:filename', (_req, res) => {
44
+ const filename = _req.params.filename;
45
+ const filepath = path_1.default.join(uploadDir, filename);
46
+ if (!fs_1.default.existsSync(filepath)) {
47
+ logger_1.default.warn(`下载失败: 文件不存在 - ${filename}`);
48
+ return res.status(404).json({ error: '文件不存在' });
49
+ }
50
+ // 记录下载次数
51
+ downloadCounts[filename] = (downloadCounts[filename] || 0) + 1;
52
+ // 记录设备信息
53
+ const clientIP = _req.ip || _req.socket.remoteAddress || 'unknown';
54
+ const userAgent = _req.get('user-agent') || 'Unknown Device';
55
+ const deviceName = parseDeviceName(userAgent);
56
+ connectedDevices.set(clientIP, {
57
+ name: deviceName,
58
+ ip: clientIP,
59
+ lastSeen: Date.now()
60
+ });
61
+ logger_1.default.info(`文件下载: ${filename} - ${clientIP}`);
62
+ res.download(filepath);
63
+ });
64
+ // #endregion
65
+ // #region 删除文件
66
+ router.delete('/delete/:filename', (_req, res) => {
67
+ const filename = _req.params.filename;
68
+ const filepath = path_1.default.join(uploadDir, filename);
69
+ if (!fs_1.default.existsSync(filepath)) {
70
+ logger_1.default.warn(`删除失败: 文件不存在 - ${filename}`);
71
+ return res.status(404).json({ error: '文件不存在' });
72
+ }
73
+ fs_1.default.unlink(filepath, (err) => {
74
+ if (err) {
75
+ logger_1.default.error(`删除文件失败: ${filename}`, err);
76
+ return res.status(500).json({ error: '删除文件失败' });
77
+ }
78
+ // 清除下载计数
79
+ delete downloadCounts[filename];
80
+ logger_1.default.info(`文件删除成功: ${filename}`);
81
+ res.json({ message: '文件删除成功' });
82
+ });
83
+ });
84
+ // #endregion
85
+ // #region 获取已连接设备
86
+ router.get('/devices', (_req, res) => {
87
+ // 清理超过5分钟未活动的设备
88
+ const now = Date.now();
89
+ const timeout = 5 * 60 * 1000; // 5分钟
90
+ for (const [ip, device] of connectedDevices.entries()) {
91
+ if (now - device.lastSeen > timeout) {
92
+ connectedDevices.delete(ip);
93
+ }
94
+ }
95
+ const devices = Array.from(connectedDevices.values());
96
+ res.json(devices);
97
+ });
98
+ // #endregion
99
+ // #region 获取服务器信息
100
+ router.get('/info', (_req, res) => {
101
+ res.json({
102
+ ip: getLocalIP(),
103
+ hostname: os_1.default.hostname(),
104
+ platform: os_1.default.platform()
105
+ });
106
+ });
107
+ // #endregion
108
+ return router;
109
+ }
110
+ // #region 辅助函数
111
+ function getLocalIP() {
112
+ const interfaces = os_1.default.networkInterfaces();
113
+ for (const name of Object.keys(interfaces)) {
114
+ const iface = interfaces[name];
115
+ if (!iface)
116
+ continue;
117
+ for (const addr of iface) {
118
+ if (addr.family === 'IPv4' && !addr.internal) {
119
+ return addr.address;
120
+ }
121
+ }
122
+ }
123
+ return '127.0.0.1';
124
+ }
125
+ function parseDeviceName(userAgent) {
126
+ if (userAgent.includes('Android')) {
127
+ const match = userAgent.match(/Android.*?;\s*([^)]+)/);
128
+ return match ? match[1].trim() : 'Android Device';
129
+ }
130
+ if (userAgent.includes('iPhone'))
131
+ return 'iPhone';
132
+ if (userAgent.includes('iPad'))
133
+ return 'iPad';
134
+ if (userAgent.includes('Mac'))
135
+ return 'MacBook Pro';
136
+ if (userAgent.includes('Windows'))
137
+ return 'Windows PC';
138
+ if (userAgent.includes('Linux'))
139
+ return 'Linux PC';
140
+ return 'Unknown Device';
141
+ }
142
+ // #endregion