Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Lint

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20

- name: Install dependencies
run: npm install --ignore-scripts

- name: Lint
run: npm run lint
11 changes: 11 additions & 0 deletions .htmlhintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"tagname-lowercase": true,
"attr-lowercase": true,
"attr-value-double-quotes": true,
"doctype-first": false,
"tag-pair": true,
"spec-char-escape": true,
"id-unique": true,
"src-not-empty": true,
"title-require": false
}
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npx lint-staged
10 changes: 10 additions & 0 deletions .stylelintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "stylelint-config-standard",
"rules": {
"no-empty-source": null,
"selector-class-pattern": null,
"color-function-notation": null,
"alpha-value-notation": null,
"rule-empty-line-before": null
}
}
2 changes: 2 additions & 0 deletions entitlements.plist
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
</dict>
</plist>
53 changes: 53 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import js from '@eslint/js'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import globals from 'globals'

export default [
js.configs.recommended,
{
files: ['src/**/*.{js,jsx}'],
plugins: {
react,
'react-hooks': reactHooks,
},
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
...globals.browser,
...globals.node,
},
parserOptions: {
ecmaFeatures: { jsx: true },
},
},
settings: {
react: { version: 'detect' },
},
rules: {
...reactHooks.configs.recommended.rules,
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'no-unused-vars': ['warn', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
}],
'react/jsx-uses-vars': 'error',
'no-empty': ['error', { allowEmptyCatch: true }],
'no-console': 'off',
},
},
{
files: ['*.js', '*.mjs', 'scripts/**/*.js'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: globals.node,
},
},
{
ignores: ['.webpack/**', 'out/**', 'node_modules/**'],
},
]
4 changes: 4 additions & 0 deletions forge.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ module.exports = {
packagerConfig: {
asar: true,
icon: path.join(__dirname, 'icon'),
extendInfo: {
NSLocationWhenInUseUsageDescription: 'Parachute needs your location to center the map on your current position.',
NSLocationUsageDescription: 'Parachute needs your location to center the map on your current position.',
},
extraResource: [
path.join(__dirname, 'bin', binaryName),
],
Expand Down
131 changes: 74 additions & 57 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,59 +1,76 @@
{
"name": "parachute",
"productName": "Parachute",
"version": "0.3.5",
"description": "My Electron application description",
"repository": {
"type": "git",
"url": "https://github.com/Harrilee/parachute.git"
},
"main": ".webpack/main",
"scripts": {
"postinstall": "bash scripts/download-go-ios.sh",
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish",
"lint": "echo \"No linting configured\""
},
"devDependencies": {
"@babel/core": "^7.25.7",
"@babel/preset-react": "^7.25.7",
"@electron-forge/cli": "^7.5.0",
"@electron-forge/maker-deb": "^7.5.0",
"@electron-forge/maker-pkg": "^7.11.1",
"@electron-forge/maker-rpm": "^7.5.0",
"@electron-forge/maker-squirrel": "^7.5.0",
"@electron-forge/maker-zip": "^7.5.0",
"@electron-forge/plugin-auto-unpack-natives": "^7.5.0",
"@electron-forge/plugin-fuses": "^7.5.0",
"@electron-forge/plugin-webpack": "^7.5.0",
"@electron/fuses": "^1.8.0",
"@electron/notarize": "^2.5.0",
"@electron/osx-sign": "^1.3.3",
"@vercel/webpack-asset-relocator-loader": "1.7.3",
"babel-loader": "^9.2.1",
"css-loader": "^6.0.0",
"electron": "32.1.2",
"file-loader": "^6.2.0",
"node-loader": "^2.0.0",
"style-loader": "^3.0.0"
},
"keywords": [],
"author": {
"name": "Harry Lee",
"email": "harrileeeee@gmail.com"
},
"license": "MIT",
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@mui/material": "^6.1.2",
"electron-squirrel-startup": "^1.0.1",
"leaflet": "^1.9.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"update-electron-app": "^3.1.2"
}
"name": "parachute",
"productName": "Parachute",
"version": "0.3.6",
"description": "My Electron application description",
"repository": {
"type": "git",
"url": "https://github.com/Harrilee/parachute.git"
},
"main": ".webpack/main",
"scripts": {
"postinstall": "bash scripts/download-go-ios.sh",
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish",
"lint": "eslint src/ && stylelint 'src/**/*.css' && htmlhint src/*.html",
"lint:fix": "eslint src/ --fix && stylelint 'src/**/*.css' --fix",
"prepare": "husky"
},
"devDependencies": {
"@babel/core": "^7.25.7",
"@babel/preset-react": "^7.25.7",
"@electron-forge/cli": "^7.5.0",
"@electron-forge/maker-deb": "^7.5.0",
"@electron-forge/maker-pkg": "^7.11.1",
"@electron-forge/maker-rpm": "^7.5.0",
"@electron-forge/maker-squirrel": "^7.5.0",
"@electron-forge/maker-zip": "^7.5.0",
"@electron-forge/plugin-auto-unpack-natives": "^7.5.0",
"@electron-forge/plugin-fuses": "^7.5.0",
"@electron-forge/plugin-webpack": "^7.5.0",
"@electron/fuses": "^1.8.0",
"@electron/notarize": "^2.5.0",
"@electron/osx-sign": "^1.3.3",
"@eslint/js": "^9.39.4",
"@vercel/webpack-asset-relocator-loader": "1.7.3",
"babel-loader": "^9.2.1",
"css-loader": "^6.0.0",
"electron": "32.1.2",
"eslint": "^9.39.4",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"file-loader": "^6.2.0",
"globals": "^17.4.0",
"htmlhint": "^1.9.2",
"husky": "^9.1.7",
"lint-staged": "^15.5.2",
"node-loader": "^2.0.0",
"style-loader": "^3.0.0",
"stylelint": "^16.26.1",
"stylelint-config-standard": "^36.0.1"
},
"keywords": [],
"author": {
"name": "Harry Lee",
"email": "harrileeeee@gmail.com"
},
"license": "MIT",
"lint-staged": {
"src/**/*.{js,jsx}": "eslint --fix",
"src/**/*.css": "stylelint --fix",
"src/**/*.html": "htmlhint"
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@mui/material": "^6.1.2",
"electron-squirrel-startup": "^1.0.1",
"leaflet": "^1.9.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"update-electron-app": "^3.1.2"
}
}
83 changes: 81 additions & 2 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react'
import { useState } from 'react'
import { createRoot } from 'react-dom/client'
import AMap from './pages/AMap.jsx'
import OpenStreetMap from './pages/OpenStreetMap.jsx'
Expand All @@ -11,11 +11,89 @@ const darkTheme = createTheme({
mode: 'light',
},
})
import _ from './app.css'
import './app.css'
import { PHONE_CONNECTION_STATUS, LOCATION_SIMULATION_STATUS } from './utils.js'

const root = createRoot(document.getElementById('root'))

const LocateButton = ({ map }) => {
const [locating, setLocating] = useState(false)

const centerMap = (latitude, longitude, source) => {
console.log(`[Locate] Centering map (${source}): lat=${latitude}, lon=${longitude}, map=${map}`)
if (map === '高德地图' && window.map) {
window.map.setCenter([longitude, latitude])
window.map.setZoom(15)
console.log('[Locate] AMap centered')
} else if (window.leafletMap) {
window.leafletMap.setView([latitude, longitude], 15)
console.log('[Locate] Leaflet centered')
} else {
console.warn('[Locate] No map instance found')
}
setLocating(false)
}

const handleLocate = () => {
console.log('[Locate] Button clicked')
setLocating(true)
let resolved = false

const ipPromise = fetch('https://get.geojs.io/v1/ip/geo.json')
.then(r => r.json())
.then(data => ({ latitude: Number(data.latitude), longitude: Number(data.longitude), source: 'IP' }))
.catch(err => { console.error('[Locate] IP geolocation failed:', err); return null })

if (navigator.geolocation) {
console.log('[Locate] navigator.geolocation available, requesting position...')
navigator.geolocation.getCurrentPosition(
pos => {
resolved = true
console.log(`[Locate] GPS success: lat=${pos.coords.latitude}, lon=${pos.coords.longitude}, accuracy=${pos.coords.accuracy}m`)
centerMap(pos.coords.latitude, pos.coords.longitude, 'GPS')
},
async err => {
console.warn(`[Locate] Geolocation denied/failed: code=${err.code}, message=${err.message}`)
if (resolved) return
const result = await ipPromise
if (!resolved && result) {
resolved = true
console.log('[Locate] Using pre-fetched IP location')
centerMap(result.latitude, result.longitude, 'IP')
} else {
setLocating(false)
}
},
{ enableHighAccuracy: true, timeout: 5000 },
)
} else {
console.warn('[Locate] navigator.geolocation not available')
ipPromise.then(result => {
if (result) centerMap(result.latitude, result.longitude, 'IP')
else setLocating(false)
})
}
}

return (
<button className="locate-btn" onClick={handleLocate} disabled={locating} title="回到当前位置">
{locating ? (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ animation: 'spin 1s linear infinite' }}>
<circle cx="12" cy="12" r="10" strokeDasharray="32" strokeDashoffset="12" />
</svg>
) : (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="4" />
<line x1="12" y1="2" x2="12" y2="6" />
<line x1="12" y1="18" x2="12" y2="22" />
<line x1="2" y1="12" x2="6" y2="12" />
<line x1="18" y1="12" x2="22" y2="12" />
</svg>
)}
</button>
)
}

const App = () => {
const [longitude, setLongitude] = useState('-')
const [latitude, setLatitude] = useState('-')
Expand Down Expand Up @@ -45,6 +123,7 @@ const App = () => {
setLocationSimulationStatus={setLocationSimulationStatus}
/>
<MapSource map={map} setMap={setMap} />
<LocateButton map={map} />
</>
)}
</ThemeProvider>
Expand Down
35 changes: 35 additions & 0 deletions src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,38 @@ button:disabled {
margin-top: 12px;
width: 100%;
}

.locate-btn {
position: absolute;
bottom: 24px;
right: 12px;
z-index: 500;
width: 36px;
height: 36px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(223, 230, 234, 0.8);
backdrop-filter: blur(3px);
color: #171717;
border: none;
border-radius: 4px;
cursor: pointer;
box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, 0.05);
}

.locate-btn:hover {
background-color: rgba(200, 210, 216, 0.9);
}

.locate-btn:disabled {
background-color: rgba(223, 230, 234, 0.8);
color: #999;
cursor: wait;
}

@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
Loading
Loading