├── .gitignore
├── .idea
├── .gitignore
├── .name
├── AirPods Sanity.iml
├── codeStyles
│ └── codeStyleConfig.xml
├── dbnavigator.xml
├── misc.xml
├── modules.xml
├── vcs.xml
└── xcode.xml
├── .vscode
└── settings.json
├── AirPods Sanity.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ │ └── Package.resolved
│ └── xcuserdata
│ │ └── tobias.xcuserdatad
│ │ ├── UserInterfaceState.xcuserstate
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── xcuserdata
│ └── tobias.xcuserdatad
│ ├── xcdebugger
│ └── Breakpoints_v2.xcbkptlist
│ └── xcschemes
│ ├── AirPods Sanity (Release).xcscheme
│ ├── AirPods Sanity.xcscheme
│ └── xcschememanagement.plist
├── AirPods Sanity
├── AirPodsObserver.swift
├── AirPodsSanityApp.swift
├── AirPods_Sanity.entitlements
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── appicon.png
│ │ ├── icon16.png
│ │ └── icon32.png
│ └── Contents.json
├── ContentView.swift
├── DevicesObserver.swift
├── Events
│ ├── Event.swift
│ ├── EventHandlerWrapper.swift
│ ├── IDisposable.swift
│ └── IInvocable.swift
├── MenuBar.swift
├── Preferences.swift
├── PreferencesFile.swift
├── PreferencesLoader.swift
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── airpods-icon.png
└── airpods-icon@2x.png
├── AirPods-Sanity-Info.plist
├── README.md
├── de.lproj
└── Localizable.strings
└── en.lproj
└── Localizable.strings
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | .DS_Store
7 | buildServer.json
8 |
9 | # User-specific files
10 | *.rsuser
11 | *.suo
12 | *.user
13 | *.userosscache
14 | *.sln.docstates
15 |
16 | # User-specific files (MonoDevelop/Xamarin Studio)
17 | *.userprefs
18 |
19 | # Mono auto generated files
20 | mono_crash.*
21 |
22 | # Build results
23 | [Dd]ebug/
24 | [Dd]ebugPublic/
25 | [Rr]elease/
26 | [Rr]eleases/
27 | x64/
28 | x86/
29 | [Ww][Ii][Nn]32/
30 | [Aa][Rr][Mm]/
31 | [Aa][Rr][Mm]64/
32 | bld/
33 | [Bb]in/
34 | [Oo]bj/
35 | [Ll]og/
36 | [Ll]ogs/
37 |
38 | # Visual Studio 2015/2017 cache/options directory
39 | .vs/
40 | # Uncomment if you have tasks that create the project's static files in wwwroot
41 | #wwwroot/
42 |
43 | # Visual Studio 2017 auto generated files
44 | Generated\ Files/
45 |
46 | # MSTest test Results
47 | [Tt]est[Rr]esult*/
48 | [Bb]uild[Ll]og.*
49 |
50 | # NUnit
51 | *.VisualState.xml
52 | TestResult.xml
53 | nunit-*.xml
54 |
55 | # Build Results of an ATL Project
56 | [Dd]ebugPS/
57 | [Rr]eleasePS/
58 | dlldata.c
59 |
60 | # Benchmark Results
61 | BenchmarkDotNet.Artifacts/
62 |
63 | # .NET Core
64 | project.lock.json
65 | project.fragment.lock.json
66 | artifacts/
67 |
68 | # ASP.NET Scaffolding
69 | ScaffoldingReadMe.txt
70 |
71 | # StyleCop
72 | StyleCopReport.xml
73 |
74 | # Files built by Visual Studio
75 | *_i.c
76 | *_p.c
77 | *_h.h
78 | *.ilk
79 | *.meta
80 | *.obj
81 | *.iobj
82 | *.pch
83 | *.pdb
84 | *.ipdb
85 | *.pgc
86 | *.pgd
87 | *.rsp
88 | *.sbr
89 | *.tlb
90 | *.tli
91 | *.tlh
92 | *.tmp
93 | *.tmp_proj
94 | *_wpftmp.csproj
95 | *.log
96 | *.vspscc
97 | *.vssscc
98 | .builds
99 | *.pidb
100 | *.svclog
101 | *.scc
102 |
103 | # Chutzpah Test files
104 | _Chutzpah*
105 |
106 | # Visual C++ cache files
107 | ipch/
108 | *.aps
109 | *.ncb
110 | *.opendb
111 | *.opensdf
112 | *.sdf
113 | *.cachefile
114 | *.VC.db
115 | *.VC.VC.opendb
116 |
117 | # Visual Studio profiler
118 | *.psess
119 | *.vsp
120 | *.vspx
121 | *.sap
122 |
123 | # Visual Studio Trace Files
124 | *.e2e
125 |
126 | # TFS 2012 Local Workspace
127 | $tf/
128 |
129 | # Guidance Automation Toolkit
130 | *.gpState
131 |
132 | # ReSharper is a .NET coding add-in
133 | _ReSharper*/
134 | *.[Rr]e[Ss]harper
135 | *.DotSettings.user
136 |
137 | # TeamCity is a build add-in
138 | _TeamCity*
139 |
140 | # DotCover is a Code Coverage Tool
141 | *.dotCover
142 |
143 | # AxoCover is a Code Coverage Tool
144 | .axoCover/*
145 | !.axoCover/settings.json
146 |
147 | # Coverlet is a free, cross platform Code Coverage Tool
148 | coverage*.json
149 | coverage*.xml
150 | coverage*.info
151 |
152 | # Visual Studio code coverage results
153 | *.coverage
154 | *.coveragexml
155 |
156 | # NCrunch
157 | _NCrunch_*
158 | .*crunch*.local.xml
159 | nCrunchTemp_*
160 |
161 | # MightyMoose
162 | *.mm.*
163 | AutoTest.Net/
164 |
165 | # Web workbench (sass)
166 | .sass-cache/
167 |
168 | # Installshield output folder
169 | [Ee]xpress/
170 |
171 | # DocProject is a documentation generator add-in
172 | DocProject/buildhelp/
173 | DocProject/Help/*.HxT
174 | DocProject/Help/*.HxC
175 | DocProject/Help/*.hhc
176 | DocProject/Help/*.hhk
177 | DocProject/Help/*.hhp
178 | DocProject/Help/Html2
179 | DocProject/Help/html
180 |
181 | # Click-Once directory
182 | publish/
183 |
184 | # Publish Web Output
185 | *.[Pp]ublish.xml
186 | *.azurePubxml
187 | # Note: Comment the next line if you want to checkin your web deploy settings,
188 | # but database connection strings (with potential passwords) will be unencrypted
189 | *.pubxml
190 | *.publishproj
191 |
192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
193 | # checkin your Azure Web App publish settings, but sensitive information contained
194 | # in these scripts will be unencrypted
195 | PublishScripts/
196 |
197 | # NuGet Packages
198 | *.nupkg
199 | # NuGet Symbol Packages
200 | *.snupkg
201 | # The packages folder can be ignored because of Package Restore
202 | **/[Pp]ackages/*
203 | # except build/, which is used as an MSBuild target.
204 | !**/[Pp]ackages/build/
205 | # Uncomment if necessary however generally it will be regenerated when needed
206 | #!**/[Pp]ackages/repositories.config
207 | # NuGet v3's project.json files produces more ignorable files
208 | *.nuget.props
209 | *.nuget.targets
210 |
211 | # Microsoft Azure Build Output
212 | csx/
213 | *.build.csdef
214 |
215 | # Microsoft Azure Emulator
216 | ecf/
217 | rcf/
218 |
219 | # Windows Store app package directories and files
220 | AppPackages/
221 | BundleArtifacts/
222 | Package.StoreAssociation.xml
223 | _pkginfo.txt
224 | *.appx
225 | *.appxbundle
226 | *.appxupload
227 |
228 | # Visual Studio cache files
229 | # files ending in .cache can be ignored
230 | *.[Cc]ache
231 | # but keep track of directories ending in .cache
232 | !?*.[Cc]ache/
233 |
234 | # Others
235 | ClientBin/
236 | ~$*
237 | *~
238 | *.dbmdl
239 | *.dbproj.schemaview
240 | *.jfm
241 | *.pfx
242 | *.publishsettings
243 | orleans.codegen.cs
244 |
245 | # Including strong name files can present a security risk
246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
247 | #*.snk
248 |
249 | # Since there are multiple workflows, uncomment next line to ignore bower_components
250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
251 | #bower_components/
252 |
253 | # RIA/Silverlight projects
254 | Generated_Code/
255 |
256 | # Backup & report files from converting an old project file
257 | # to a newer Visual Studio version. Backup files are not needed,
258 | # because we have git ;-)
259 | _UpgradeReport_Files/
260 | Backup*/
261 | UpgradeLog*.XML
262 | UpgradeLog*.htm
263 | ServiceFabricBackup/
264 | *.rptproj.bak
265 |
266 | # SQL Server files
267 | *.mdf
268 | *.ldf
269 | *.ndf
270 |
271 | # Business Intelligence projects
272 | *.rdl.data
273 | *.bim.layout
274 | *.bim_*.settings
275 | *.rptproj.rsuser
276 | *- [Bb]ackup.rdl
277 | *- [Bb]ackup ([0-9]).rdl
278 | *- [Bb]ackup ([0-9][0-9]).rdl
279 |
280 | # Microsoft Fakes
281 | FakesAssemblies/
282 |
283 | # GhostDoc plugin setting file
284 | *.GhostDoc.xml
285 |
286 | # Node.js Tools for Visual Studio
287 | .ntvs_analysis.dat
288 | node_modules/
289 |
290 | # Visual Studio 6 build log
291 | *.plg
292 |
293 | # Visual Studio 6 workspace options file
294 | *.opt
295 |
296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
297 | *.vbw
298 |
299 | # Visual Studio LightSwitch build output
300 | **/*.HTMLClient/GeneratedArtifacts
301 | **/*.DesktopClient/GeneratedArtifacts
302 | **/*.DesktopClient/ModelManifest.xml
303 | **/*.Server/GeneratedArtifacts
304 | **/*.Server/ModelManifest.xml
305 | _Pvt_Extensions
306 |
307 | # Paket dependency manager
308 | .paket/paket.exe
309 | paket-files/
310 |
311 | # FAKE - F# Make
312 | .fake/
313 |
314 | # CodeRush personal settings
315 | .cr/personal
316 |
317 | # Python Tools for Visual Studio (PTVS)
318 | __pycache__/
319 | *.pyc
320 |
321 | # Cake - Uncomment if you are using it
322 | # tools/**
323 | # !tools/packages.config
324 |
325 | # Tabs Studio
326 | *.tss
327 |
328 | # Telerik's JustMock configuration file
329 | *.jmconfig
330 |
331 | # BizTalk build output
332 | *.btp.cs
333 | *.btm.cs
334 | *.odx.cs
335 | *.xsd.cs
336 |
337 | # OpenCover UI analysis results
338 | OpenCover/
339 |
340 | # Azure Stream Analytics local run output
341 | ASALocalRun/
342 |
343 | # MSBuild Binary and Structured Log
344 | *.binlog
345 |
346 | # NVidia Nsight GPU debugger configuration file
347 | *.nvuser
348 |
349 | # MFractors (Xamarin productivity tool) working folder
350 | .mfractor/
351 |
352 | # Local History for Visual Studio
353 | .localhistory/
354 |
355 | # BeatPulse healthcheck temp database
356 | healthchecksdb
357 |
358 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
359 | MigrationBackup/
360 |
361 | # Ionide (cross platform F# VS Code tools) working folder
362 | .ionide/
363 |
364 | # Fody - auto-generated XML schema
365 | FodyWeavers.xsd
366 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | AirPods Sanity
--------------------------------------------------------------------------------
/.idea/AirPods Sanity.iml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/dbnavigator.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 |
462 |
463 |
464 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/xcode.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.quickSuggestions": {
3 | "comments": "off",
4 | "strings": "off",
5 | "other": "off"
6 | }
7 | }
--------------------------------------------------------------------------------
/AirPods Sanity.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 55;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | CC3C7243988D2BD3ABE984FE /* EventHandlerWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3C7DD2C4380FA265019917 /* EventHandlerWrapper.swift */; };
11 | CC3C72DF8E2CA00596ACCC72 /* MenuBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3C76D635F1C824B78A4181 /* MenuBar.swift */; };
12 | CC3C751346DA2A1EC6EEFA12 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3C7EF198CC3419A5B8EBAE /* Preferences.swift */; };
13 | CC3C76F52EBC16FD13B1D764 /* DevicesObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3C7A1237F3148E067F970D /* DevicesObserver.swift */; };
14 | CC3C77633D8431061E9F93D3 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3C70AF31CDB8C76C345F6D /* Event.swift */; };
15 | CC3C77FDBEAD90BF9EFE680A /* PreferencesLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3C785B6595C413114BCC8A /* PreferencesLoader.swift */; };
16 | CC3C7CA3469FFA756A55A8EE /* IInvocable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3C7D45D626F1DCBDB0BF5F /* IInvocable.swift */; };
17 | CC3C7CDDC79C66F63827F4A6 /* IDisposable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3C7D5383B08626365DF7DD /* IDisposable.swift */; };
18 | F63BA8282BEBEA6000AC8EBA /* PreferencesFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = F63BA8272BEBEA6000AC8EBA /* PreferencesFile.swift */; };
19 | F6B118522BEB93B400B991EF /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = F6B118512BEB93B400B991EF /* LaunchAtLogin */; };
20 | F6B9613328B82761001E1D00 /* AirPodsSanityApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B9613228B82761001E1D00 /* AirPodsSanityApp.swift */; };
21 | F6B9613528B82761001E1D00 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B9613428B82761001E1D00 /* ContentView.swift */; };
22 | F6B9613728B82762001E1D00 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F6B9613628B82762001E1D00 /* Assets.xcassets */; };
23 | F6B9613A28B82762001E1D00 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F6B9613928B82762001E1D00 /* Preview Assets.xcassets */; };
24 | F6BCD64428BA781500C56ACE /* airpods-icon.png in Resources */ = {isa = PBXBuildFile; fileRef = F6BCD64328BA781500C56ACE /* airpods-icon.png */; };
25 | F6BCD64628BA782100C56ACE /* airpods-icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F6BCD64528BA782100C56ACE /* airpods-icon@2x.png */; };
26 | F6BCD64928BA8F5100C56ACE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F6BCD64B28BA8F5100C56ACE /* Localizable.strings */; };
27 | F6F5183B28B84239000552D3 /* AirPodsObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6F5183A28B84239000552D3 /* AirPodsObserver.swift */; };
28 | F6F5183D28B84B00000552D3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6F5183C28B84B00000552D3 /* AppDelegate.swift */; };
29 | F6F5184328B85EF9000552D3 /* SimplyCoreAudio in Frameworks */ = {isa = PBXBuildFile; productRef = F6F5184228B85EF9000552D3 /* SimplyCoreAudio */; };
30 | /* End PBXBuildFile section */
31 |
32 | /* Begin PBXFileReference section */
33 | CC3C70AF31CDB8C76C345F6D /* Event.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; };
34 | CC3C76D635F1C824B78A4181 /* MenuBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuBar.swift; sourceTree = ""; };
35 | CC3C785B6595C413114BCC8A /* PreferencesLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesLoader.swift; sourceTree = ""; };
36 | CC3C7A1237F3148E067F970D /* DevicesObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DevicesObserver.swift; sourceTree = ""; };
37 | CC3C7D45D626F1DCBDB0BF5F /* IInvocable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IInvocable.swift; sourceTree = ""; };
38 | CC3C7D5383B08626365DF7DD /* IDisposable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IDisposable.swift; sourceTree = ""; };
39 | CC3C7DD2C4380FA265019917 /* EventHandlerWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventHandlerWrapper.swift; sourceTree = ""; };
40 | CC3C7EF198CC3419A5B8EBAE /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; };
41 | F63BA8272BEBEA6000AC8EBA /* PreferencesFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesFile.swift; sourceTree = ""; };
42 | F6B9612F28B82761001E1D00 /* AirPods Sanity.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "AirPods Sanity.app"; sourceTree = BUILT_PRODUCTS_DIR; };
43 | F6B9613228B82761001E1D00 /* AirPodsSanityApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirPodsSanityApp.swift; sourceTree = ""; };
44 | F6B9613428B82761001E1D00 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
45 | F6B9613628B82762001E1D00 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
46 | F6B9613928B82762001E1D00 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
47 | F6B9613B28B82762001E1D00 /* AirPods_Sanity.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AirPods_Sanity.entitlements; sourceTree = ""; };
48 | F6BCD64328BA781500C56ACE /* airpods-icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "airpods-icon.png"; sourceTree = ""; };
49 | F6BCD64528BA782100C56ACE /* airpods-icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "airpods-icon@2x.png"; sourceTree = ""; };
50 | F6BCD64A28BA8F5100C56ACE /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; };
51 | F6BCD64C28BA8F9900C56ACE /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; };
52 | F6F5183A28B84239000552D3 /* AirPodsObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirPodsObserver.swift; sourceTree = ""; };
53 | F6F5183C28B84B00000552D3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
54 | F6F5184028B85D23000552D3 /* AirPods-Sanity-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.info; path = "AirPods-Sanity-Info.plist"; sourceTree = SOURCE_ROOT; };
55 | /* End PBXFileReference section */
56 |
57 | /* Begin PBXFrameworksBuildPhase section */
58 | F6B9612C28B82761001E1D00 /* Frameworks */ = {
59 | isa = PBXFrameworksBuildPhase;
60 | buildActionMask = 2147483647;
61 | files = (
62 | F6B118522BEB93B400B991EF /* LaunchAtLogin in Frameworks */,
63 | F6F5184328B85EF9000552D3 /* SimplyCoreAudio in Frameworks */,
64 | );
65 | runOnlyForDeploymentPostprocessing = 0;
66 | };
67 | /* End PBXFrameworksBuildPhase section */
68 |
69 | /* Begin PBXGroup section */
70 | CC3C7F472754E14F32187B3D /* Events */ = {
71 | isa = PBXGroup;
72 | children = (
73 | CC3C7D5383B08626365DF7DD /* IDisposable.swift */,
74 | CC3C7DD2C4380FA265019917 /* EventHandlerWrapper.swift */,
75 | CC3C70AF31CDB8C76C345F6D /* Event.swift */,
76 | CC3C7D45D626F1DCBDB0BF5F /* IInvocable.swift */,
77 | );
78 | path = Events;
79 | sourceTree = "";
80 | };
81 | F6B9612628B82761001E1D00 = {
82 | isa = PBXGroup;
83 | children = (
84 | F6BCD64B28BA8F5100C56ACE /* Localizable.strings */,
85 | F6B9613128B82761001E1D00 /* AirPods Sanity */,
86 | F6B9613028B82761001E1D00 /* Products */,
87 | );
88 | sourceTree = "";
89 | };
90 | F6B9613028B82761001E1D00 /* Products */ = {
91 | isa = PBXGroup;
92 | children = (
93 | F6B9612F28B82761001E1D00 /* AirPods Sanity.app */,
94 | );
95 | name = Products;
96 | sourceTree = "";
97 | };
98 | F6B9613128B82761001E1D00 /* AirPods Sanity */ = {
99 | isa = PBXGroup;
100 | children = (
101 | F6F5184028B85D23000552D3 /* AirPods-Sanity-Info.plist */,
102 | F6BCD64328BA781500C56ACE /* airpods-icon.png */,
103 | F6BCD64528BA782100C56ACE /* airpods-icon@2x.png */,
104 | F6F5183C28B84B00000552D3 /* AppDelegate.swift */,
105 | F6B9613228B82761001E1D00 /* AirPodsSanityApp.swift */,
106 | F6B9613428B82761001E1D00 /* ContentView.swift */,
107 | F6F5183A28B84239000552D3 /* AirPodsObserver.swift */,
108 | F6B9613628B82762001E1D00 /* Assets.xcassets */,
109 | F6B9613B28B82762001E1D00 /* AirPods_Sanity.entitlements */,
110 | F6B9613828B82762001E1D00 /* Preview Content */,
111 | CC3C7A1237F3148E067F970D /* DevicesObserver.swift */,
112 | CC3C7F472754E14F32187B3D /* Events */,
113 | CC3C7EF198CC3419A5B8EBAE /* Preferences.swift */,
114 | F63BA8272BEBEA6000AC8EBA /* PreferencesFile.swift */,
115 | CC3C785B6595C413114BCC8A /* PreferencesLoader.swift */,
116 | CC3C76D635F1C824B78A4181 /* MenuBar.swift */,
117 | );
118 | path = "AirPods Sanity";
119 | sourceTree = "";
120 | };
121 | F6B9613828B82762001E1D00 /* Preview Content */ = {
122 | isa = PBXGroup;
123 | children = (
124 | F6B9613928B82762001E1D00 /* Preview Assets.xcassets */,
125 | );
126 | path = "Preview Content";
127 | sourceTree = "";
128 | };
129 | /* End PBXGroup section */
130 |
131 | /* Begin PBXNativeTarget section */
132 | F6B9612E28B82761001E1D00 /* AirPods Sanity */ = {
133 | isa = PBXNativeTarget;
134 | buildConfigurationList = F6B9613E28B82762001E1D00 /* Build configuration list for PBXNativeTarget "AirPods Sanity" */;
135 | buildPhases = (
136 | F6B9612B28B82761001E1D00 /* Sources */,
137 | F6B9612C28B82761001E1D00 /* Frameworks */,
138 | F6B9612D28B82761001E1D00 /* Resources */,
139 | );
140 | buildRules = (
141 | );
142 | dependencies = (
143 | );
144 | name = "AirPods Sanity";
145 | packageProductDependencies = (
146 | F6F5184228B85EF9000552D3 /* SimplyCoreAudio */,
147 | F6B118512BEB93B400B991EF /* LaunchAtLogin */,
148 | );
149 | productName = "AirPods Sanity";
150 | productReference = F6B9612F28B82761001E1D00 /* AirPods Sanity.app */;
151 | productType = "com.apple.product-type.application";
152 | };
153 | /* End PBXNativeTarget section */
154 |
155 | /* Begin PBXProject section */
156 | F6B9612728B82761001E1D00 /* Project object */ = {
157 | isa = PBXProject;
158 | attributes = {
159 | BuildIndependentTargetsInParallel = 1;
160 | LastSwiftUpdateCheck = 1340;
161 | LastUpgradeCheck = 1530;
162 | ORGANIZATIONNAME = "Tobias Punke";
163 | TargetAttributes = {
164 | F6B9612E28B82761001E1D00 = {
165 | CreatedOnToolsVersion = 13.4.1;
166 | };
167 | };
168 | };
169 | buildConfigurationList = F6B9612A28B82761001E1D00 /* Build configuration list for PBXProject "AirPods Sanity" */;
170 | compatibilityVersion = "Xcode 13.0";
171 | developmentRegion = en;
172 | hasScannedForEncodings = 0;
173 | knownRegions = (
174 | en,
175 | de,
176 | Base,
177 | );
178 | mainGroup = F6B9612628B82761001E1D00;
179 | packageReferences = (
180 | F64E7EEC28B8290000B70C49 /* XCRemoteSwiftPackageReference "SimplyCoreAudio" */,
181 | F6F5184128B85EF9000552D3 /* XCRemoteSwiftPackageReference "SimplyCoreAudio" */,
182 | F6B118502BEB93B400B991EF /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */,
183 | );
184 | productRefGroup = F6B9613028B82761001E1D00 /* Products */;
185 | projectDirPath = "";
186 | projectRoot = "";
187 | targets = (
188 | F6B9612E28B82761001E1D00 /* AirPods Sanity */,
189 | );
190 | };
191 | /* End PBXProject section */
192 |
193 | /* Begin PBXResourcesBuildPhase section */
194 | F6B9612D28B82761001E1D00 /* Resources */ = {
195 | isa = PBXResourcesBuildPhase;
196 | buildActionMask = 2147483647;
197 | files = (
198 | F6BCD64928BA8F5100C56ACE /* Localizable.strings in Resources */,
199 | F6BCD64628BA782100C56ACE /* airpods-icon@2x.png in Resources */,
200 | F6B9613A28B82762001E1D00 /* Preview Assets.xcassets in Resources */,
201 | F6BCD64428BA781500C56ACE /* airpods-icon.png in Resources */,
202 | F6B9613728B82762001E1D00 /* Assets.xcassets in Resources */,
203 | );
204 | runOnlyForDeploymentPostprocessing = 0;
205 | };
206 | /* End PBXResourcesBuildPhase section */
207 |
208 | /* Begin PBXSourcesBuildPhase section */
209 | F6B9612B28B82761001E1D00 /* Sources */ = {
210 | isa = PBXSourcesBuildPhase;
211 | buildActionMask = 2147483647;
212 | files = (
213 | F6F5183D28B84B00000552D3 /* AppDelegate.swift in Sources */,
214 | F6B9613528B82761001E1D00 /* ContentView.swift in Sources */,
215 | F6F5183B28B84239000552D3 /* AirPodsObserver.swift in Sources */,
216 | F6B9613328B82761001E1D00 /* AirPodsSanityApp.swift in Sources */,
217 | CC3C76F52EBC16FD13B1D764 /* DevicesObserver.swift in Sources */,
218 | F63BA8282BEBEA6000AC8EBA /* PreferencesFile.swift in Sources */,
219 | CC3C7CDDC79C66F63827F4A6 /* IDisposable.swift in Sources */,
220 | CC3C7243988D2BD3ABE984FE /* EventHandlerWrapper.swift in Sources */,
221 | CC3C77633D8431061E9F93D3 /* Event.swift in Sources */,
222 | CC3C7CA3469FFA756A55A8EE /* IInvocable.swift in Sources */,
223 | CC3C751346DA2A1EC6EEFA12 /* Preferences.swift in Sources */,
224 | CC3C77FDBEAD90BF9EFE680A /* PreferencesLoader.swift in Sources */,
225 | CC3C72DF8E2CA00596ACCC72 /* MenuBar.swift in Sources */,
226 | );
227 | runOnlyForDeploymentPostprocessing = 0;
228 | };
229 | /* End PBXSourcesBuildPhase section */
230 |
231 | /* Begin PBXVariantGroup section */
232 | F6BCD64B28BA8F5100C56ACE /* Localizable.strings */ = {
233 | isa = PBXVariantGroup;
234 | children = (
235 | F6BCD64A28BA8F5100C56ACE /* en */,
236 | F6BCD64C28BA8F9900C56ACE /* de */,
237 | );
238 | name = Localizable.strings;
239 | sourceTree = "";
240 | };
241 | /* End PBXVariantGroup section */
242 |
243 | /* Begin XCBuildConfiguration section */
244 | F6B9613C28B82762001E1D00 /* Debug */ = {
245 | isa = XCBuildConfiguration;
246 | buildSettings = {
247 | ALWAYS_SEARCH_USER_PATHS = NO;
248 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
249 | CLANG_ANALYZER_NONNULL = YES;
250 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
251 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
252 | CLANG_ENABLE_MODULES = YES;
253 | CLANG_ENABLE_OBJC_ARC = YES;
254 | CLANG_ENABLE_OBJC_WEAK = YES;
255 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
256 | CLANG_WARN_BOOL_CONVERSION = YES;
257 | CLANG_WARN_COMMA = YES;
258 | CLANG_WARN_CONSTANT_CONVERSION = YES;
259 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
260 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
261 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
262 | CLANG_WARN_EMPTY_BODY = YES;
263 | CLANG_WARN_ENUM_CONVERSION = YES;
264 | CLANG_WARN_INFINITE_RECURSION = YES;
265 | CLANG_WARN_INT_CONVERSION = YES;
266 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
267 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
268 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
269 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
270 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
271 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
272 | CLANG_WARN_STRICT_PROTOTYPES = YES;
273 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
274 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
275 | CLANG_WARN_UNREACHABLE_CODE = YES;
276 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
277 | COPY_PHASE_STRIP = NO;
278 | DEAD_CODE_STRIPPING = YES;
279 | DEBUG_INFORMATION_FORMAT = dwarf;
280 | ENABLE_STRICT_OBJC_MSGSEND = YES;
281 | ENABLE_TESTABILITY = YES;
282 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
283 | GCC_C_LANGUAGE_STANDARD = gnu11;
284 | GCC_DYNAMIC_NO_PIC = NO;
285 | GCC_NO_COMMON_BLOCKS = YES;
286 | GCC_OPTIMIZATION_LEVEL = 0;
287 | GCC_PREPROCESSOR_DEFINITIONS = (
288 | "DEBUG=1",
289 | "$(inherited)",
290 | );
291 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
292 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
293 | GCC_WARN_UNDECLARED_SELECTOR = YES;
294 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
295 | GCC_WARN_UNUSED_FUNCTION = YES;
296 | GCC_WARN_UNUSED_VARIABLE = YES;
297 | MACOSX_DEPLOYMENT_TARGET = 13.0;
298 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
299 | MTL_FAST_MATH = YES;
300 | ONLY_ACTIVE_ARCH = YES;
301 | SDKROOT = macosx;
302 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
303 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
304 | };
305 | name = Debug;
306 | };
307 | F6B9613D28B82762001E1D00 /* Release */ = {
308 | isa = XCBuildConfiguration;
309 | buildSettings = {
310 | ALWAYS_SEARCH_USER_PATHS = NO;
311 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
312 | CLANG_ANALYZER_NONNULL = YES;
313 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
314 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
315 | CLANG_ENABLE_MODULES = YES;
316 | CLANG_ENABLE_OBJC_ARC = YES;
317 | CLANG_ENABLE_OBJC_WEAK = YES;
318 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
319 | CLANG_WARN_BOOL_CONVERSION = YES;
320 | CLANG_WARN_COMMA = YES;
321 | CLANG_WARN_CONSTANT_CONVERSION = YES;
322 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
323 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
324 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
325 | CLANG_WARN_EMPTY_BODY = YES;
326 | CLANG_WARN_ENUM_CONVERSION = YES;
327 | CLANG_WARN_INFINITE_RECURSION = YES;
328 | CLANG_WARN_INT_CONVERSION = YES;
329 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
330 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
331 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
332 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
333 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
334 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
335 | CLANG_WARN_STRICT_PROTOTYPES = YES;
336 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
337 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
338 | CLANG_WARN_UNREACHABLE_CODE = YES;
339 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
340 | COPY_PHASE_STRIP = NO;
341 | DEAD_CODE_STRIPPING = YES;
342 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
343 | ENABLE_NS_ASSERTIONS = NO;
344 | ENABLE_STRICT_OBJC_MSGSEND = YES;
345 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
346 | GCC_C_LANGUAGE_STANDARD = gnu11;
347 | GCC_NO_COMMON_BLOCKS = YES;
348 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
349 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
350 | GCC_WARN_UNDECLARED_SELECTOR = YES;
351 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
352 | GCC_WARN_UNUSED_FUNCTION = YES;
353 | GCC_WARN_UNUSED_VARIABLE = YES;
354 | MACOSX_DEPLOYMENT_TARGET = 13.0;
355 | MTL_ENABLE_DEBUG_INFO = NO;
356 | MTL_FAST_MATH = YES;
357 | SDKROOT = macosx;
358 | SWIFT_COMPILATION_MODE = wholemodule;
359 | SWIFT_OPTIMIZATION_LEVEL = "-O";
360 | };
361 | name = Release;
362 | };
363 | F6B9613F28B82762001E1D00 /* Debug */ = {
364 | isa = XCBuildConfiguration;
365 | buildSettings = {
366 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
367 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
368 | CODE_SIGN_ENTITLEMENTS = "AirPods Sanity/AirPods_Sanity.entitlements";
369 | CODE_SIGN_IDENTITY = "-";
370 | CODE_SIGN_STYLE = Automatic;
371 | COMBINE_HIDPI_IMAGES = YES;
372 | CURRENT_PROJECT_VERSION = 1.0.4.2;
373 | DEAD_CODE_STRIPPING = YES;
374 | DEVELOPMENT_ASSET_PATHS = "\"AirPods Sanity/Preview Content\"";
375 | DEVELOPMENT_TEAM = 2P9YZ8CQSH;
376 | ENABLE_HARDENED_RUNTIME = YES;
377 | ENABLE_PREVIEWS = YES;
378 | GENERATE_INFOPLIST_FILE = YES;
379 | INFOPLIST_FILE = "AirPods-Sanity-Info.plist";
380 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
381 | LD_RUNPATH_SEARCH_PATHS = (
382 | "$(inherited)",
383 | "@executable_path/../Frameworks",
384 | );
385 | MARKETING_VERSION = 1.0;
386 | PRODUCT_BUNDLE_IDENTIFIER = "eu.punke.AirPods-Sanity";
387 | PRODUCT_NAME = "$(TARGET_NAME)";
388 | SWIFT_EMIT_LOC_STRINGS = YES;
389 | SWIFT_VERSION = 5.0;
390 | };
391 | name = Debug;
392 | };
393 | F6B9614028B82762001E1D00 /* Release */ = {
394 | isa = XCBuildConfiguration;
395 | buildSettings = {
396 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
397 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
398 | CODE_SIGN_ENTITLEMENTS = "AirPods Sanity/AirPods_Sanity.entitlements";
399 | CODE_SIGN_IDENTITY = "-";
400 | CODE_SIGN_STYLE = Automatic;
401 | COMBINE_HIDPI_IMAGES = YES;
402 | CURRENT_PROJECT_VERSION = 1.0.4.2;
403 | DEAD_CODE_STRIPPING = YES;
404 | DEVELOPMENT_ASSET_PATHS = "\"AirPods Sanity/Preview Content\"";
405 | DEVELOPMENT_TEAM = 2P9YZ8CQSH;
406 | ENABLE_HARDENED_RUNTIME = YES;
407 | ENABLE_PREVIEWS = YES;
408 | GENERATE_INFOPLIST_FILE = YES;
409 | INFOPLIST_FILE = "AirPods-Sanity-Info.plist";
410 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
411 | LD_RUNPATH_SEARCH_PATHS = (
412 | "$(inherited)",
413 | "@executable_path/../Frameworks",
414 | );
415 | MARKETING_VERSION = 1.0;
416 | PRODUCT_BUNDLE_IDENTIFIER = "eu.punke.AirPods-Sanity";
417 | PRODUCT_NAME = "$(TARGET_NAME)";
418 | SWIFT_EMIT_LOC_STRINGS = YES;
419 | SWIFT_VERSION = 5.0;
420 | };
421 | name = Release;
422 | };
423 | /* End XCBuildConfiguration section */
424 |
425 | /* Begin XCConfigurationList section */
426 | F6B9612A28B82761001E1D00 /* Build configuration list for PBXProject "AirPods Sanity" */ = {
427 | isa = XCConfigurationList;
428 | buildConfigurations = (
429 | F6B9613C28B82762001E1D00 /* Debug */,
430 | F6B9613D28B82762001E1D00 /* Release */,
431 | );
432 | defaultConfigurationIsVisible = 0;
433 | defaultConfigurationName = Release;
434 | };
435 | F6B9613E28B82762001E1D00 /* Build configuration list for PBXNativeTarget "AirPods Sanity" */ = {
436 | isa = XCConfigurationList;
437 | buildConfigurations = (
438 | F6B9613F28B82762001E1D00 /* Debug */,
439 | F6B9614028B82762001E1D00 /* Release */,
440 | );
441 | defaultConfigurationIsVisible = 0;
442 | defaultConfigurationName = Release;
443 | };
444 | /* End XCConfigurationList section */
445 |
446 | /* Begin XCRemoteSwiftPackageReference section */
447 | F64E7EEC28B8290000B70C49 /* XCRemoteSwiftPackageReference "SimplyCoreAudio" */ = {
448 | isa = XCRemoteSwiftPackageReference;
449 | repositoryURL = "https://github.com/rnine/SimplyCoreAudio.git";
450 | requirement = {
451 | kind = upToNextMajorVersion;
452 | minimumVersion = 4.0.0;
453 | };
454 | };
455 | F6B118502BEB93B400B991EF /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */ = {
456 | isa = XCRemoteSwiftPackageReference;
457 | repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin-Modern";
458 | requirement = {
459 | branch = main;
460 | kind = branch;
461 | };
462 | };
463 | F6F5184128B85EF9000552D3 /* XCRemoteSwiftPackageReference "SimplyCoreAudio" */ = {
464 | isa = XCRemoteSwiftPackageReference;
465 | repositoryURL = "https://github.com/rnine/SimplyCoreAudio.git";
466 | requirement = {
467 | branch = develop;
468 | kind = branch;
469 | };
470 | };
471 | /* End XCRemoteSwiftPackageReference section */
472 |
473 | /* Begin XCSwiftPackageProductDependency section */
474 | F6B118512BEB93B400B991EF /* LaunchAtLogin */ = {
475 | isa = XCSwiftPackageProductDependency;
476 | package = F6B118502BEB93B400B991EF /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */;
477 | productName = LaunchAtLogin;
478 | };
479 | F6F5184228B85EF9000552D3 /* SimplyCoreAudio */ = {
480 | isa = XCSwiftPackageProductDependency;
481 | package = F6F5184128B85EF9000552D3 /* XCRemoteSwiftPackageReference "SimplyCoreAudio" */;
482 | productName = SimplyCoreAudio;
483 | };
484 | /* End XCSwiftPackageProductDependency section */
485 | };
486 | rootObject = F6B9612728B82761001E1D00 /* Project object */;
487 | }
488 |
--------------------------------------------------------------------------------
/AirPods Sanity.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/AirPods Sanity.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/AirPods Sanity.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "309fd4cfd1c6d504fa689a1a6dc9fef048deacc2409e6da59ab5ec38e372b412",
3 | "pins" : [
4 | {
5 | "identity" : "launchatlogin-modern",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/sindresorhus/LaunchAtLogin-Modern",
8 | "state" : {
9 | "branch" : "main",
10 | "revision" : "a04ec1c363be3627734f6dad757d82f5d4fa8fcc"
11 | }
12 | },
13 | {
14 | "identity" : "simplycoreaudio",
15 | "kind" : "remoteSourceControl",
16 | "location" : "https://github.com/rnine/SimplyCoreAudio.git",
17 | "state" : {
18 | "branch" : "develop",
19 | "revision" : "343d463cffef1f30458d02ce2dc441138e9e0134"
20 | }
21 | },
22 | {
23 | "identity" : "swift-atomics",
24 | "kind" : "remoteSourceControl",
25 | "location" : "https://github.com/apple/swift-atomics.git",
26 | "state" : {
27 | "revision" : "3e95ba32cd1b4c877f6163e8eea54afc4e63bf9f",
28 | "version" : "0.0.3"
29 | }
30 | }
31 | ],
32 | "version" : 3
33 | }
34 |
--------------------------------------------------------------------------------
/AirPods Sanity.xcodeproj/project.xcworkspace/xcuserdata/tobias.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gaulomatic/AirPodsSanity/b7a645f54f1f4979cca31d38aaaf9fad6750eb09/AirPods Sanity.xcodeproj/project.xcworkspace/xcuserdata/tobias.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/AirPods Sanity.xcodeproj/project.xcworkspace/xcuserdata/tobias.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/AirPods Sanity.xcodeproj/xcuserdata/tobias.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
9 |
21 |
22 |
23 |
25 |
37 |
38 |
39 |
41 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/AirPods Sanity.xcodeproj/xcuserdata/tobias.xcuserdatad/xcschemes/AirPods Sanity (Release).xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
42 |
44 |
50 |
51 |
52 |
55 |
56 |
57 |
63 |
64 |
66 |
67 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/AirPods Sanity.xcodeproj/xcuserdata/tobias.xcuserdatad/xcschemes/AirPods Sanity.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
56 |
57 |
58 |
64 |
65 |
67 |
68 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/AirPods Sanity.xcodeproj/xcuserdata/tobias.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | AirPods Sanity.xcscheme
8 |
9 | orderHint
10 | 0
11 |
12 | AirPods Sanity (Release).xcscheme
13 |
14 | orderHint
15 | 1
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/AirPods Sanity/AirPodsObserver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ObservableSCA.swift
3 | // AirPods Sanity
4 | //
5 | // Created by Tobias Punke on 25.08.22.
6 | //
7 |
8 | import Foundation
9 | import SimplyCoreAudio
10 |
11 | class AirPodsObserver: ObservableObject
12 | {
13 | private let _Preferences: Preferences
14 | private let _Simply: SimplyCoreAudio
15 | private let _NotificationCenter: NotificationCenter
16 |
17 | private var _Observers: [NSObjectProtocol]
18 |
19 | private var _DefaultInputDeviceName: String?
20 |
21 | init()
22 | {
23 | self._Preferences = Preferences.Instance
24 | self._Simply = SimplyCoreAudio()
25 | self._NotificationCenter = NotificationCenter.default
26 | self._Observers = []
27 |
28 | self.UpdateDefaultInputDevice()
29 | self.AddObservers()
30 | }
31 |
32 | deinit
33 | {
34 | self.RemoveObservers()
35 | }
36 | }
37 |
38 | private extension AirPodsObserver
39 | {
40 | func UpdateDefaultInputDevice()
41 | {
42 | guard let __DefaultInputDevice = self._Simply.defaultInputDevice else { return }
43 | guard let _ = self._Preferences.AirPodsDeviceNames.filter({ $0 == __DefaultInputDevice.name }).first else
44 | {
45 | self._DefaultInputDeviceName = __DefaultInputDevice.name
46 | return
47 | }
48 |
49 | if !self._Preferences.IsEnabled
50 | {
51 | return
52 | }
53 |
54 | guard let __NewInputDeviceName = self._DefaultInputDeviceName != nil ? self._DefaultInputDeviceName : self._Preferences.InputDeviceName else { return }
55 | guard let __InputDevice = self._Simply.allInputDevices.filter({ $0.name == __NewInputDeviceName }).first else { return }
56 |
57 | if __DefaultInputDevice.id != __InputDevice.id
58 | {
59 | self.RemoveObservers()
60 | __InputDevice.isDefaultInputDevice = true
61 | self.AddObservers()
62 | }
63 |
64 | let __Seconds = 10.0
65 |
66 | DispatchQueue.main.asyncAfter(deadline: .now() + __Seconds)
67 | {
68 | guard let __DefaultOutputDevice = self._Simply.defaultOutputDevice else { return }
69 | guard let __SampleRates = __DefaultOutputDevice.nominalSampleRates?.sorted(by: { $0 > $1 }) else { return }
70 |
71 | self.RemoveObservers()
72 | __DefaultOutputDevice.setNominalSampleRate(__SampleRates[0])
73 | self.AddObservers()
74 | }
75 | }
76 |
77 | func AddObservers()
78 | {
79 | self._Observers.append(contentsOf:[
80 | self._NotificationCenter.addObserver(forName: .defaultInputDeviceChanged, object: nil, queue: .main) { (_) in
81 | self.UpdateDefaultInputDevice()
82 | },
83 | ])
84 | }
85 |
86 | func RemoveObservers()
87 | {
88 | for __Observer in self._Observers
89 | {
90 | self._NotificationCenter.removeObserver(__Observer)
91 | }
92 |
93 | self._Observers.removeAll()
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/AirPods Sanity/AirPodsSanityApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AirPodsSanityApp.swift
3 | // AirPods Sanity
4 | //
5 | // Created by Tobias Punke on 25.08.22.
6 | //
7 | // https://sarunw.com/posts/swiftui-menu-bar-app/
8 | // https://sarunw.com/posts/how-to-make-macos-menu-bar-app/
9 | // https://github.com/rnine/SimplyCoreAudio
10 | //
11 |
12 | import SwiftUI
13 |
14 | @main
15 | struct AirPodsSanityApp: App
16 | {
17 | @NSApplicationDelegateAdaptor(AppDelegate.self) private var _AppDelegate
18 |
19 | @StateObject private var _AirPodsObserver = AirPodsObserver()
20 |
21 | var body: some Scene
22 | {
23 | WindowGroup
24 | {
25 | ContentView()
26 | .environmentObject(self._AirPodsObserver)
27 | .hidden()
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/AirPods Sanity/AirPods_Sanity.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 | com.apple.security.device.audio-input
10 |
11 | com.apple.security.device.usb
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/AirPods Sanity/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // AirPods Sanity
4 | //
5 | // Created by Tobias Punke on 26.08.22.
6 | //
7 |
8 | import AppKit
9 | import Foundation
10 | import SimplyCoreAudio
11 |
12 | class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject
13 | {
14 | private let NotificationName = Notification.Name("eu.punke.AirPods-Sanity.AppLaunched")
15 |
16 | private let _Preferences: Preferences
17 |
18 | private var _Devices: DevicesObserver!
19 | private var _MenuBar: MenuBar!
20 | private var _Subscription: IDisposable!
21 |
22 | override init()
23 | {
24 | self._Preferences = Preferences.Instance
25 | }
26 |
27 | func applicationDidFinishLaunching(_ notification: Notification)
28 | {
29 | if self.IsAlreadyRunning()
30 | {
31 | self.SendLaunchNotification()
32 | NSApp.terminate(nil)
33 | }
34 | else
35 | {
36 | SetupApplication()
37 | }
38 | }
39 | func applicationDidBecomeActive(_ notification: Notification)
40 | {
41 | }
42 |
43 | func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool
44 | {
45 | // https://stackoverflow.com/a/59003304/2239781
46 | let __AppleEvent: NSAppleEventDescriptor? = NSAppleEventManager.shared().currentAppleEvent
47 | let __SenderName: String = __AppleEvent?.attributeDescriptor(forKeyword: keyAddressAttr)?.stringValue ?? ""
48 | // let __SenderPID: Int32? = __AppleEvent?.attributeDescriptor(forKeyword: keySenderPIDAttr)?.int32Value ?? 0
49 |
50 | if __SenderName != "Dock"
51 | {
52 | self.PerformSecondLaunchActions()
53 | }
54 | else if !self._Preferences.ShowInDock
55 | {
56 | self.PerformSecondLaunchActions()
57 | }
58 |
59 | return false
60 | }
61 |
62 | func applicationWillTerminate(_ notification: Notification)
63 | {
64 | if self._Subscription != nil
65 | {
66 | self._Subscription.dispose()
67 | }
68 | }
69 |
70 | private func IsAlreadyRunning() -> Bool
71 | {
72 | let __RunningApps = NSWorkspace.shared.runningApplications
73 | let __CurrentApp = Bundle.main.bundleIdentifier!
74 |
75 | return __RunningApps.filter { $0.bundleIdentifier == __CurrentApp }.count > 1
76 | }
77 |
78 | private func SendLaunchNotification()
79 | {
80 | DistributedNotificationCenter.default().post(name: NotificationName, object: nil)
81 | }
82 |
83 | private func SetupLaunchNotificationListener()
84 | {
85 | DistributedNotificationCenter.default().addObserver(self, selector: #selector(HandleSecondLaunch), name: NotificationName, object: nil)
86 | }
87 |
88 | @objc private func HandleSecondLaunch()
89 | {
90 | self.PerformSecondLaunchActions()
91 | }
92 |
93 | private func PerformSecondLaunchActions()
94 | {
95 | if self._MenuBar.IsVisible
96 | {
97 | return
98 | }
99 |
100 | self._MenuBar.Show()
101 |
102 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.0)
103 | {
104 | self._MenuBar.Show()
105 | }
106 |
107 | DispatchQueue.main.asyncAfter(deadline: .now() + 2.0)
108 | {
109 | self._MenuBar.Show()
110 | }
111 |
112 | DispatchQueue.main.asyncAfter(deadline: .now() + 10.0)
113 | {
114 | if !self._Preferences.ShowInMenuBar
115 | {
116 | self._MenuBar.Hide()
117 | }
118 | }
119 | }
120 |
121 | private func SetupApplication()
122 | {
123 | // Hide the window when the application finishes launching
124 | if let window = NSApplication.shared.windows.first
125 | {
126 | window.orderOut(self)
127 | }
128 |
129 | self._MenuBar = MenuBar()
130 | self._Devices = DevicesObserver()
131 | self._Subscription = self._Devices.InputDevicesChanged.addHandler(target: self, handler: AppDelegate.OnInputDevicesChanged)
132 |
133 | self._MenuBar.CreateMenu()
134 |
135 | self.SetupLaunchNotificationListener()
136 | }
137 |
138 | func OnInputDevicesChanged(data: [AudioDevice])
139 | {
140 | let __IsMenuVisible = self._MenuBar.IsVisible
141 | self._MenuBar.CreateMenu()
142 |
143 | if __IsMenuVisible
144 | {
145 | self._MenuBar.Show()
146 | }
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/AirPods Sanity/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/AirPods Sanity/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "16x16",
5 | "idiom" : "mac",
6 | "filename" : "icon16.png",
7 | "scale" : "1x"
8 | },
9 | {
10 | "size" : "16x16",
11 | "idiom" : "mac",
12 | "filename" : "icon32.png",
13 | "scale" : "2x"
14 | },
15 | {
16 | "idiom" : "mac",
17 | "size" : "32x32",
18 | "scale" : "1x"
19 | },
20 | {
21 | "idiom" : "mac",
22 | "size" : "32x32",
23 | "scale" : "2x"
24 | },
25 | {
26 | "idiom" : "mac",
27 | "size" : "128x128",
28 | "scale" : "1x"
29 | },
30 | {
31 | "idiom" : "mac",
32 | "size" : "128x128",
33 | "scale" : "2x"
34 | },
35 | {
36 | "idiom" : "mac",
37 | "size" : "256x256",
38 | "scale" : "1x"
39 | },
40 | {
41 | "idiom" : "mac",
42 | "size" : "256x256",
43 | "scale" : "2x"
44 | },
45 | {
46 | "size" : "512x512",
47 | "idiom" : "mac",
48 | "filename" : "appicon.png",
49 | "scale" : "1x"
50 | },
51 | {
52 | "idiom" : "mac",
53 | "size" : "512x512",
54 | "scale" : "2x"
55 | }
56 | ],
57 | "info" : {
58 | "version" : 1,
59 | "author" : "xcode"
60 | }
61 | }
--------------------------------------------------------------------------------
/AirPods Sanity/Assets.xcassets/AppIcon.appiconset/appicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gaulomatic/AirPodsSanity/b7a645f54f1f4979cca31d38aaaf9fad6750eb09/AirPods Sanity/Assets.xcassets/AppIcon.appiconset/appicon.png
--------------------------------------------------------------------------------
/AirPods Sanity/Assets.xcassets/AppIcon.appiconset/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gaulomatic/AirPodsSanity/b7a645f54f1f4979cca31d38aaaf9fad6750eb09/AirPods Sanity/Assets.xcassets/AppIcon.appiconset/icon16.png
--------------------------------------------------------------------------------
/AirPods Sanity/Assets.xcassets/AppIcon.appiconset/icon32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gaulomatic/AirPodsSanity/b7a645f54f1f4979cca31d38aaaf9fad6750eb09/AirPods Sanity/Assets.xcassets/AppIcon.appiconset/icon32.png
--------------------------------------------------------------------------------
/AirPods Sanity/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/AirPods Sanity/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // AirPods Sanity
4 | //
5 | // Created by Tobias Punke on 25.08.22.
6 | //
7 |
8 | import SwiftUI
9 | import AppKit
10 |
11 | struct ContentView: View
12 | {
13 | var body: some View
14 | {
15 | Text("Keep your sanity in check!")
16 | .padding()
17 | }
18 | }
19 |
20 | struct ContentView_Previews: PreviewProvider
21 | {
22 | static var previews: some View
23 | {
24 | ContentView()
25 | .environmentObject(AirPodsObserver())
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/AirPods Sanity/DevicesObserver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ObservableSCA.swift
3 | // AirPods Sanity
4 | //
5 | // Created by Tobias Punke on 25.08.22.
6 | //
7 |
8 | import Foundation
9 | import SimplyCoreAudio
10 |
11 | class DevicesObserver: ObservableObject
12 | {
13 | let InputDevicesChanged = Event<[AudioDevice]>()
14 |
15 | private let _Simply = SimplyCoreAudio()
16 | private var _Observers = [NSObjectProtocol]()
17 | private let _NotificationCenter = NotificationCenter.default
18 |
19 | init()
20 | {
21 | self.UpdateDefaultInputDevice()
22 | self.UpdateDefaultOutputDevice()
23 | self.UpdateDefaultSystemDevice()
24 |
25 | self.AddObservers()
26 | }
27 |
28 | deinit
29 | {
30 | self.RemoveObservers()
31 | }
32 | }
33 |
34 | internal extension DevicesObserver
35 | {
36 | func UpdateDefaultInputDevice()
37 | {
38 | }
39 |
40 | func UpdateDefaultOutputDevice()
41 | {
42 | }
43 |
44 | func UpdateDefaultSystemDevice()
45 | {
46 | }
47 |
48 | private func OnDeviceListChanged()
49 | {
50 | self.InputDevicesChanged.raise(data: self._Simply.allInputDevices)
51 | }
52 |
53 | func AddObservers()
54 | {
55 | self._Observers.append(contentsOf: [
56 | self._NotificationCenter.addObserver(forName: .deviceListChanged, object: nil, queue: .main) { (notification) in
57 | self.OnDeviceListChanged()
58 | },
59 |
60 | self._NotificationCenter.addObserver(forName: .defaultInputDeviceChanged, object: nil, queue: .main) { (_) in
61 | self.UpdateDefaultInputDevice()
62 | },
63 |
64 | self._NotificationCenter.addObserver(forName: .defaultOutputDeviceChanged, object: nil, queue: .main) { (_) in
65 | self.UpdateDefaultOutputDevice()
66 | },
67 |
68 | self._NotificationCenter.addObserver(forName: .defaultSystemOutputDeviceChanged, object: nil, queue: .main) { (_) in
69 | self.UpdateDefaultSystemDevice()
70 | },
71 |
72 | self._NotificationCenter.addObserver(forName: .deviceNominalSampleRateDidChange, object: nil, queue: .main) { (notification) in
73 | },
74 |
75 | self._NotificationCenter.addObserver(forName: .deviceClockSourceDidChange, object: nil, queue: .main) { (notification) in
76 | },
77 | ])
78 | }
79 |
80 | func RemoveObservers()
81 | {
82 | for __Observer in self._Observers
83 | {
84 | self._NotificationCenter.removeObserver(__Observer)
85 | }
86 |
87 | self._Observers.removeAll()
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/AirPods Sanity/Events/Event.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Tobias Punke on 27.08.22.
3 | //
4 |
5 | import Foundation
6 |
7 | public class Event
8 | {
9 | public typealias EventHandler = (T) -> ()
10 |
11 | var eventHandlers = [IInvocable]()
12 |
13 | public func raise(data: T)
14 | {
15 | for handler in self.eventHandlers
16 | {
17 | handler.invoke(data: data)
18 | }
19 | }
20 |
21 | public func addHandler(target: U, handler: @escaping (U) -> EventHandler) -> IDisposable
22 | {
23 | let wrapper = EventHandlerWrapper(target: target, handler: handler, event: self)
24 | eventHandlers.append(wrapper)
25 | return wrapper
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/AirPods Sanity/Events/EventHandlerWrapper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Tobias Punke on 27.08.22.
3 | //
4 |
5 | import Foundation
6 |
7 | class EventHandlerWrapper : IInvocable, IDisposable
8 | {
9 | weak var target: T?
10 | let handler: (T) -> (U) -> ()
11 | let event: Event
12 |
13 | init(target: T?, handler: @escaping (T) -> (U) -> (), event: Event)
14 | {
15 | self.target = target
16 | self.handler = handler
17 | self.event = event;
18 | }
19 |
20 | func invoke(data: Any) -> ()
21 | {
22 | if let t = target
23 | {
24 | handler(t)(data as! U)
25 | }
26 | }
27 |
28 | func dispose()
29 | {
30 | event.eventHandlers = event.eventHandlers.filter
31 | {
32 | $0 !== self
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/AirPods Sanity/Events/IDisposable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Tobias Punke on 27.08.22.
3 | //
4 |
5 | import Foundation
6 |
7 | public protocol IDisposable
8 | {
9 | func dispose()
10 | }
11 |
--------------------------------------------------------------------------------
/AirPods Sanity/Events/IInvocable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Tobias Punke on 27.08.22.
3 | //
4 |
5 | import Foundation
6 |
7 | protocol IInvocable: AnyObject
8 | {
9 | func invoke(data: Any)
10 | }
11 |
--------------------------------------------------------------------------------
/AirPods Sanity/MenuBar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Tobias Punke on 27.08.22.
3 | //
4 |
5 | import AppKit
6 | import Foundation
7 | import LaunchAtLogin
8 | import SimplyCoreAudio
9 |
10 | class MenuBar
11 | {
12 | private let _SimplyCoreAudio: SimplyCoreAudio
13 | private let _Preferences: Preferences
14 |
15 | private var _InputDeviceItems: [NSMenuItem]
16 | private var _OutputDeviceItems: [NSMenuItem]
17 |
18 | private var _StatusBarItem: NSStatusItem
19 |
20 | init()
21 | {
22 | self._SimplyCoreAudio = SimplyCoreAudio()
23 | self._Preferences = Preferences.Instance
24 |
25 | self._InputDeviceItems = []
26 | self._OutputDeviceItems = []
27 |
28 | self._StatusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
29 |
30 | self.CreateStatusItem()
31 | self.SetShowInMenuBar()
32 | self.SetShowInDock()
33 | }
34 |
35 | private func CreateStatusItem()
36 | {
37 | let __Image = NSImage(named: "airpods-icon")
38 |
39 | __Image?.isTemplate = true
40 |
41 | if let __Button = self._StatusBarItem.button
42 | {
43 | __Button.toolTip = NSLocalizedString("MenuBar.ToolTip", comment: "")
44 |
45 | if __Image != nil
46 | {
47 | __Button.image = __Image
48 | }
49 | else
50 | {
51 | __Button.title = NSLocalizedString("MenuBar.ToolTip", comment: "")
52 | }
53 | }
54 | }
55 |
56 | public func CreateMenu()
57 | {
58 | self._InputDeviceItems.removeAll()
59 | self._InputDeviceItems = self.CreateInputDeviceItems(simply: self._SimplyCoreAudio, preferences: self._Preferences)
60 |
61 | self._OutputDeviceItems.removeAll()
62 | self._OutputDeviceItems = self.CreateOutputDeviceItems(simply: self._SimplyCoreAudio, preferences: self._Preferences)
63 |
64 | let __Menu = NSMenu()
65 |
66 | __Menu.addItem(self.CreateIsEnabledItem(preferences: self._Preferences))
67 |
68 | __Menu.addItem(NSMenuItem.separator())
69 | __Menu.addItem(self.CreateLaunchOnLoginItem(preferences: self._Preferences))
70 | __Menu.addItem(self.CreateShowInMenuBarItem(preferences: self._Preferences))
71 | __Menu.addItem(self.CreateShowInDockItem(preferences: self._Preferences))
72 |
73 | __Menu.addItem(NSMenuItem.separator())
74 | self.AddItems(menu: __Menu, items: self._InputDeviceItems, label: NSLocalizedString("MenuBar.InputDevices", comment: ""))
75 |
76 | __Menu.addItem(NSMenuItem.separator())
77 | self.AddItems(menu: __Menu, items: self._OutputDeviceItems, label: NSLocalizedString("MenuBar.OutputDevices", comment: ""))
78 |
79 | __Menu.addItem(NSMenuItem.separator())
80 | __Menu.addItem(self.CreateQuitApplicationItem())
81 |
82 | self.SetShowInMenuBar()
83 | self._StatusBarItem.menu = __Menu
84 | }
85 |
86 | public var IsVisible: Bool
87 | {
88 | get
89 | {
90 | return self._StatusBarItem.isVisible
91 | }
92 | }
93 |
94 | public func Show()
95 | {
96 | self._StatusBarItem.isVisible = true;
97 | }
98 |
99 | public func Hide()
100 | {
101 | self._StatusBarItem.isVisible = false;
102 | }
103 |
104 | private func SetShowInMenuBar()
105 | {
106 | self._StatusBarItem.isVisible = self._Preferences.ShowInMenuBar
107 | }
108 |
109 | private func SetLaunchOnLogin()
110 | {
111 | LaunchAtLogin.isEnabled = self._Preferences.LaunchOnLogin
112 | }
113 |
114 | private func SetShowInDock()
115 | {
116 | let __Preferences = self._Preferences
117 |
118 | if __Preferences.ShowInDock
119 | {
120 | // The application is an ordinary app that appears in the Dock and may
121 | // have a user interface.
122 | NSApp.setActivationPolicy(.regular)
123 | }
124 | else
125 | {
126 | // The application does not appear in the Dock and may not create
127 | // windows or be activated.
128 | NSApp.setActivationPolicy(.prohibited)
129 | }
130 | }
131 |
132 | private func CreateLaunchOnLoginItem(preferences: Preferences) -> NSMenuItem
133 | {
134 | let __MenuItem = NSMenuItem()
135 |
136 | __MenuItem.title = NSLocalizedString("MenuBar.LaunchOnLogin", comment: "")
137 | __MenuItem.target = self
138 | __MenuItem.action = #selector(OnToggleLaunchOnLogin(_:))
139 |
140 | if preferences.LaunchOnLogin
141 | {
142 | __MenuItem.state = NSControl.StateValue.on
143 | }
144 | else
145 | {
146 | __MenuItem.state = NSControl.StateValue.off
147 | }
148 |
149 | return __MenuItem
150 | }
151 |
152 | private func CreateShowInMenuBarItem(preferences: Preferences) -> NSMenuItem
153 | {
154 | let __MenuItem = NSMenuItem()
155 |
156 | __MenuItem.title = NSLocalizedString("MenuBar.ShowInMenuBar", comment: "")
157 | __MenuItem.target = self
158 | __MenuItem.action = #selector(OnToggleShowInMenuBar(_:))
159 |
160 | if preferences.ShowInMenuBar
161 | {
162 | __MenuItem.state = NSControl.StateValue.on
163 | }
164 | else
165 | {
166 | __MenuItem.state = NSControl.StateValue.off
167 | }
168 |
169 | return __MenuItem
170 | }
171 |
172 | private func CreateShowInDockItem(preferences: Preferences) -> NSMenuItem
173 | {
174 | let __MenuItem = NSMenuItem()
175 |
176 | __MenuItem.title = NSLocalizedString("MenuBar.ShowInDock", comment: "")
177 | __MenuItem.target = self
178 | __MenuItem.action = #selector(OnToggleShowInDock(_:))
179 |
180 | if preferences.ShowInDock
181 | {
182 | __MenuItem.state = NSControl.StateValue.on
183 | }
184 | else
185 | {
186 | __MenuItem.state = NSControl.StateValue.off
187 | }
188 |
189 | return __MenuItem
190 | }
191 |
192 | private func CreateIsEnabledItem(preferences: Preferences) -> NSMenuItem
193 | {
194 | let __MenuItem = NSMenuItem()
195 |
196 | __MenuItem.title = NSLocalizedString("MenuBar.IsEnabled", comment: "")
197 | __MenuItem.target = self
198 | __MenuItem.action = #selector(OnToggleIsEnabled(_:))
199 |
200 | if preferences.IsEnabled
201 | {
202 | __MenuItem.state = NSControl.StateValue.on
203 | }
204 | else
205 | {
206 | __MenuItem.state = NSControl.StateValue.off
207 | }
208 |
209 | return __MenuItem
210 | }
211 |
212 | private func CreateQuitApplicationItem() -> NSMenuItem
213 | {
214 | let __QuitLabel = NSLocalizedString("MenuBar.Quit", comment: "")
215 | let __QuitShortcut = NSLocalizedString("MenuBar.QuitShortcut", comment: "")
216 |
217 | return NSMenuItem(title: __QuitLabel, action: #selector(NSApplication.terminate(_:)), keyEquivalent: __QuitShortcut)
218 | }
219 |
220 | private func CreateInputDeviceItems(simply: SimplyCoreAudio, preferences: Preferences) -> [NSMenuItem]
221 | {
222 | let __InputDevices = simply.allInputDevices
223 | var __MenuItems: [NSMenuItem] = []
224 |
225 | for __AudioDevice in __InputDevices
226 | {
227 | let __MenuItem = NSMenuItem()
228 |
229 | __MenuItem.title = __AudioDevice.name
230 | __MenuItem.target = self
231 | __MenuItem.action = #selector(OnSelectInputDevice(_:))
232 | __MenuItem.state = NSControl.StateValue.off
233 |
234 | if __AudioDevice.name == preferences.InputDeviceName
235 | {
236 | __MenuItem.state = NSControl.StateValue.on
237 | }
238 |
239 | __MenuItems.append(__MenuItem)
240 | }
241 |
242 | return __MenuItems.sorted(by: { $0.title < $1.title })
243 | }
244 |
245 | private func CreateOutputDeviceItems(simply: SimplyCoreAudio, preferences: Preferences) -> [NSMenuItem]
246 | {
247 | let __InputDevices = simply.allOutputDevices
248 | var __MenuItems: [NSMenuItem] = []
249 |
250 | for __AudioDevice in __InputDevices
251 | {
252 | let __MenuItem = NSMenuItem()
253 |
254 | __MenuItem.title = __AudioDevice.name
255 | __MenuItem.target = self
256 | __MenuItem.action = #selector(OnSelectOutputDevice(_:))
257 | __MenuItem.state = NSControl.StateValue.off
258 |
259 | if preferences.AirPodsDeviceNames.contains(__AudioDevice.name)
260 | {
261 | __MenuItem.state = NSControl.StateValue.on
262 | }
263 |
264 | __MenuItems.append(__MenuItem)
265 | }
266 |
267 | return __MenuItems.sorted(by: { $0.title < $1.title })
268 | }
269 |
270 | private func AddItems(menu: NSMenu, items: [NSMenuItem], label: String)
271 | {
272 | let __Label = NSMenuItem()
273 |
274 | __Label.title = label
275 | __Label.isEnabled = false
276 |
277 | menu.addItem(__Label)
278 |
279 | for __MenuItem in items
280 | {
281 | menu.addItem(__MenuItem)
282 | }
283 | }
284 |
285 | @objc private func OnToggleLaunchOnLogin(_ sender: NSMenuItem)
286 | {
287 | let __Preferences = self._Preferences
288 | let __State = sender.state
289 |
290 | if __State == NSControl.StateValue.on
291 | {
292 | __Preferences.LaunchOnLogin = false
293 | sender.state = NSControl.StateValue.off
294 | }
295 | else if __State == NSControl.StateValue.off
296 | {
297 | __Preferences.LaunchOnLogin = true
298 | sender.state = NSControl.StateValue.on
299 | }
300 |
301 | self.SetLaunchOnLogin()
302 |
303 | self._Preferences.WriteSettings()
304 | }
305 |
306 | @objc private func OnToggleShowInMenuBar(_ sender: NSMenuItem)
307 | {
308 | let __Preferences = self._Preferences
309 | let __State = sender.state
310 |
311 | if __State == NSControl.StateValue.on
312 | {
313 | __Preferences.ShowInMenuBar = false
314 | sender.state = NSControl.StateValue.off
315 | }
316 | else if __State == NSControl.StateValue.off
317 | {
318 | __Preferences.ShowInMenuBar = true
319 | sender.state = NSControl.StateValue.on
320 | }
321 |
322 | self.SetShowInMenuBar()
323 |
324 | self._Preferences.WriteSettings()
325 | }
326 |
327 | @objc private func OnToggleShowInDock(_ sender: NSMenuItem)
328 | {
329 | let __Preferences = self._Preferences
330 | let __State = sender.state
331 |
332 | if __State == NSControl.StateValue.on
333 | {
334 | __Preferences.ShowInDock = false
335 | sender.state = NSControl.StateValue.off
336 | }
337 | else if __State == NSControl.StateValue.off
338 | {
339 | __Preferences.ShowInDock = true
340 | sender.state = NSControl.StateValue.on
341 | }
342 |
343 | self.SetShowInDock()
344 |
345 | self._Preferences.WriteSettings()
346 | }
347 |
348 | @objc private func OnToggleIsEnabled(_ sender: NSMenuItem)
349 | {
350 | let __Preferences = self._Preferences
351 | let __State = sender.state
352 |
353 | if __State == NSControl.StateValue.on
354 | {
355 | __Preferences.IsEnabled = false
356 | sender.state = NSControl.StateValue.off
357 | }
358 | else if __State == NSControl.StateValue.off
359 | {
360 | __Preferences.IsEnabled = true
361 | sender.state = NSControl.StateValue.on
362 | }
363 |
364 | self._Preferences.WriteSettings()
365 | }
366 |
367 | @objc private func OnSelectInputDevice(_ sender: NSMenuItem)
368 | {
369 | let __Preferences = self._Preferences
370 | let __State = sender.state
371 |
372 | for __Item in self._InputDeviceItems
373 | {
374 | __Item.state = NSControl.StateValue.off
375 | }
376 |
377 | if __State == NSControl.StateValue.on
378 | {
379 | __Preferences.InputDeviceName = nil
380 | }
381 | else if __State == NSControl.StateValue.off
382 | {
383 | __Preferences.InputDeviceName = sender.title
384 | sender.state = NSControl.StateValue.on
385 | }
386 |
387 | self._Preferences.WriteSettings()
388 | }
389 |
390 | @objc private func OnSelectOutputDevice(_ sender: NSMenuItem)
391 | {
392 | let __Preferences = self._Preferences
393 |
394 | if sender.state == NSControl.StateValue.on
395 | {
396 | if __Preferences.AirPodsDeviceNames.contains(sender.title)
397 | {
398 | __Preferences.AirPodsDeviceNames = __Preferences.AirPodsDeviceNames.filter { $0 != sender.title }
399 | }
400 |
401 | sender.state = NSControl.StateValue.off
402 | }
403 | else if sender.state == NSControl.StateValue.off
404 | {
405 | if !__Preferences.AirPodsDeviceNames.contains(sender.title)
406 | {
407 | __Preferences.AirPodsDeviceNames.append(sender.title)
408 | }
409 |
410 | sender.state = NSControl.StateValue.on
411 | }
412 |
413 | self._Preferences.WriteSettings()
414 | }
415 | }
416 |
--------------------------------------------------------------------------------
/AirPods Sanity/Preferences.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Tobias Punke on 27.08.22.
3 | //
4 |
5 | import Foundation
6 |
7 | class Preferences
8 | {
9 | private static var _Instance: Preferences?
10 | private let _PreferencesFile: PreferencesFile
11 |
12 | private init()
13 | {
14 | self._PreferencesFile = PreferencesLoader.LoadSettings()
15 | }
16 |
17 | static var Instance: Preferences
18 | {
19 | if _Instance == nil
20 | {
21 | _Instance = Preferences()
22 | }
23 |
24 | return _Instance!
25 | }
26 |
27 | public var LaunchOnLogin: Bool
28 | {
29 | get
30 | {
31 | if let __UnWrapped = self._PreferencesFile.LaunchOnLogin
32 | {
33 | return __UnWrapped
34 | }
35 | else
36 | {
37 | return false
38 | }
39 | }
40 | set(value)
41 | {
42 | self._PreferencesFile.LaunchOnLogin = value
43 | }
44 | }
45 |
46 | public var ShowInMenuBar: Bool
47 | {
48 | get
49 | {
50 | if let __UnWrapped = self._PreferencesFile.ShowInMenuBar
51 | {
52 | return __UnWrapped
53 | }
54 | else
55 | {
56 | return true
57 | }
58 | }
59 | set(value)
60 | {
61 | self._PreferencesFile.ShowInMenuBar = value
62 | }
63 | }
64 |
65 | public var ShowInDock: Bool
66 | {
67 | get
68 | {
69 | if let __UnWrapped = self._PreferencesFile.ShowInDock
70 | {
71 | return __UnWrapped
72 | }
73 | else
74 | {
75 | return false
76 | }
77 | }
78 | set(value)
79 | {
80 | self._PreferencesFile.ShowInDock = value
81 | }
82 | }
83 |
84 | public var IsEnabled: Bool
85 | {
86 | get
87 | {
88 | if let __UnWrapped = self._PreferencesFile.IsEnabled
89 | {
90 | return __UnWrapped
91 | }
92 | else
93 | {
94 | return true
95 | }
96 | }
97 | set(value)
98 | {
99 | self._PreferencesFile.IsEnabled = value
100 | }
101 | }
102 |
103 | public var InputDeviceName: String?
104 | {
105 | get
106 | {
107 | return self._PreferencesFile.InputDeviceName
108 | }
109 | set(value)
110 | {
111 | self._PreferencesFile.InputDeviceName = value
112 | }
113 | }
114 |
115 | public var AirPodsDeviceNames: [String]
116 | {
117 | get
118 | {
119 | if let __UnWrapped = self._PreferencesFile.AirPodsDeviceNames
120 | {
121 | return __UnWrapped
122 | }
123 | else
124 | {
125 | return []
126 | }
127 | }
128 | set(value)
129 | {
130 | self._PreferencesFile.AirPodsDeviceNames = value
131 | }
132 | }
133 |
134 | public func WriteSettings()
135 | {
136 | PreferencesLoader.WriteSettings(preferences: self._PreferencesFile)
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/AirPods Sanity/PreferencesFile.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PreferencesFile.swift
3 | // AirPods Sanity
4 | //
5 | // Created by Tobias Punke on 08.05.24.
6 | // Copyright © 2024 Tobias Punke. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class PreferencesFile: Codable
12 | {
13 | var LaunchOnLogin: Bool?
14 | var ShowInMenuBar: Bool?
15 | var ShowInDock: Bool?
16 | var IsEnabled: Bool?
17 | var InputDeviceName: String?
18 | var AirPodsDeviceNames: [String]?
19 | }
20 |
--------------------------------------------------------------------------------
/AirPods Sanity/PreferencesLoader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Tobias Punke on 27.08.22.
3 | //
4 |
5 | import Foundation
6 |
7 | class PreferencesLoader
8 | {
9 | static private var PlistURL: URL
10 | {
11 | let __DocumentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
12 | return __DocumentsPath.appendingPathComponent("settings.plist")
13 | }
14 |
15 | static func LoadSettings() -> PreferencesFile
16 | {
17 | let decoder = PropertyListDecoder()
18 |
19 | guard let __Data = try? Data.init(contentsOf: PlistURL),
20 | let __Preferences = try? decoder.decode(PreferencesFile.self, from: __Data)
21 | else
22 | {
23 | return PreferencesFile()
24 | }
25 |
26 | return __Preferences
27 | }
28 |
29 | static func WriteSettings(preferences: PreferencesFile)
30 | {
31 | let __Encoder = PropertyListEncoder()
32 |
33 | if let __Data = try? __Encoder.encode(preferences)
34 | {
35 | if FileManager.default.fileExists(atPath: PlistURL.path)
36 | {
37 | // Update an existing plist
38 | try? __Data.write(to: PlistURL)
39 | }
40 | else
41 | {
42 | // Create a new plist
43 | FileManager.default.createFile(atPath: PlistURL.path, contents: __Data, attributes: nil)
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/AirPods Sanity/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/AirPods Sanity/airpods-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gaulomatic/AirPodsSanity/b7a645f54f1f4979cca31d38aaaf9fad6750eb09/AirPods Sanity/airpods-icon.png
--------------------------------------------------------------------------------
/AirPods Sanity/airpods-icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gaulomatic/AirPodsSanity/b7a645f54f1f4979cca31d38aaaf9fad6750eb09/AirPods Sanity/airpods-icon@2x.png
--------------------------------------------------------------------------------
/AirPods-Sanity-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LSUIElement
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AirPodsSanity
2 |
3 | > Credit: AirPodsSanity is inspired by [milgra/airpodssoundqualityfixer](https://github.com/milgra/airpodssoundqualityfixer)
4 |
5 | Keeps you from loosing your sanity when using AirPods with a Mac.
6 |
7 | You ever wondered, why the audio quality of your beloved AirPods can get as bad as talking to people over some wire that was built during the Apollo missions took place in the 60s? Ask no further, you came to the right place!
8 |
9 | ## Because... reasons
10 |
11 | The technical reason is simple: Bluetooth has a low bandwidth. So when Apple decided to set your AirPods microphone as the one in charge every. single. time. you connect them to your Mac, things go downhill - fast. Only Steve Jobs in his grave can answer the hard questions: Why, Apple?
12 |
13 | ## What it does
14 |
15 | What this app does is super-duper simple and trivial: Mark one or more output device as "AirPods". Whenever those come online, either the selected input device or the current system input device will be maintained.
16 |
17 | ## How it improves your life
18 |
19 | So what that means is, you can live your life in peace, harmony and appreciate the rainbows and unicorns - once this app is installed.
20 |
21 | ## What it doesn't do
22 |
23 | This piece of software is not cloud-native, has no micro service architecture, did not follow DDD principals, contains an algorithm designed by a fool, can not scale (neither vertically nor horizontally) and abuses your sense of humor.
24 |
25 | ## Features
26 |
27 | - Keeps you healthy
28 | - Makes life better
29 | - Protects your sanity
30 |
31 | ## Roadmap
32 |
33 | - Asking Steve in an upcoming session, why this is even a thing
34 |
35 | # Installation
36 |
37 | #### macOS
38 |
39 | - Download the `.dmg` from the release page: [DMG download](https://github.com/Gaulomatic/AirPodsSanity/releases)
40 | - Run the follwing command in the Terminal or iTerm: `xattr -d com.apple.quarantine "/Applications/AirPods Sanity.app"`
41 |
42 | #### Windows
43 |
44 | - You are out of luck. On the other hand, this issue only applies to macOS, so....
45 |
46 | #### Linux
47 |
48 | - You are good to go. Essentially.
49 |
50 |
51 | __Please feel free to download, fork and/or provide any feedback!__
--------------------------------------------------------------------------------
/de.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | Localizable.strings
3 | AirPods Sanity
4 |
5 | Created by Tobias Punke on 27.08.22.
6 |
7 | */
8 | "MenuBar.ToolTip" = "AirPods Sanity";
9 | "MenuBar.LaunchOnLogin" = "Beim Anmelden ausführen";
10 | "MenuBar.ShowInMenuBar" = "In der Menüleiste anzeigen";
11 | "MenuBar.ShowInDock" = "Im Dock anzeigen";
12 | "MenuBar.IsEnabled" = "Auf bevorzugtes Eingabegerät umschalten";
13 | "MenuBar.InputDevices" = "Bevorzugtes Eingabegerät:";
14 | "MenuBar.OutputDevices" = "AirPods-Geräte auswählen:";
15 | "MenuBar.Quit" = "Beenden";
16 | "MenuBar.QuitShortcut" = "b";
17 |
--------------------------------------------------------------------------------
/en.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | Localizable.strings
3 | AirPods Sanity
4 |
5 | Created by Tobias Punke on 27.08.22.
6 |
7 | */
8 | "MenuBar.ToolTip" = "AirPods Sanity";
9 | "MenuBar.LaunchOnLogin" = "Launch on login";
10 | "MenuBar.ShowInMenuBar" = "Show in Menubar";
11 | "MenuBar.ShowInDock" = "Show in Dock";
12 | "MenuBar.IsEnabled" = "Enable Sanitizer";
13 | "MenuBar.InputDevices" = "Preferred Input Device:";
14 | "MenuBar.OutputDevices" = "Which are AirPods?";
15 | "MenuBar.Quit" = "Quit";
16 | "MenuBar.QuitShortcut" = "q";
17 |
--------------------------------------------------------------------------------