Ice Cubes 2.0 + iOS 26 supports (#2280)
* iOS 26 compiles
* New compose accessory view
* Better GlassEffectContainer transition
* Fixes
* Drop iOS 17 + Timeline filters at bottom
* Glass status observer
* Fixes
* More fixes
* Disable tabbar collapse
* Refacor timeline checkpoint
* Tyding code
* Set version to 2.0
* Remove fast timeline setting
* Perf fixes
* Remove custom Sidebar
* Perf boost + remove content gradient
* Fixes
* Remove steaming
* Fix TL
* Use Tab
* Fix separators
* Better sheet
* Editor enhance
* Leading status row actions
* More iOS 26 fixes
* Fix context menu tint color
* Fix app account sheet
* Fix extension send button
* New Icons
* AppIcon
* Add back VisionOS Icon
* Working icon
* Update Claude.MD
* WIP Playground
* Replace OpenAI with FoundationModels
* Stream response
* Prewarm Assistant on editor open
* Add more LLM tools
* Fixes
* Remove contentWarning prompt
* Update packages to Swift 6.2
* Various iOS 26 fixes
* Prepare birictional gap
* Add sections for iPad sidebar
* Fix profile status update
* Make StatusesState Equatable
* Disable LLM on macCatalyst for now
* Add Tags and Lists to sideabar
* Add local timeline and tags group in the sidebar
* Account: Refine header view
* Show non LLM tag
* Support V2 notifications group
* Refactor NotificationsViewModel
* Fix initial gap
* Notifications list: Add glassEffect
* Set editor glass effect to interactive
* Refactor TimelineView
* Notifications: Merge new group
* Notifications: Refactor VM
* Notifications: Cleanup code
* Liquid glass DM View
* Add direct access to DM in notifications top menu
* Editor: Fix sizing on mention/tags
* Better DM detail view
* DM: More refactor view
* DM: Even better views
* DM: Rework media attachements
* DM: Interactive textfield
* Notifications: Refactor and remove ViewModel
* DM: Refactor and remove ViewModel
* DM Detail: Refactor without ViewModel
* Remove sub.club support
* DM Detail: Fix loading
* Icons cleanup
* Tweak icons
* Account Detail: Refactor sub tabs
* More icons
* Account Detail: Remove ViewModel
* Account Detail: Refactor to smaller views
* Explore: Refactor to components + inline state
* Accounts Statuses: Refactor + remove VM
* Edit note: Remove VM
* Trim AccountDetail
* Account: Set button to primary color
* Fixes
* Improve UI consistency and context menus in media and notifications
Refactored MediaView to use context menus instead of Menu for media items and updated alt/discard marker buttons for iOS 26 compatibility. Added .tint(.label) to notification filter buttons for consistent appearance. Minor code style and import order improvements in AppView.
* Add .glassProminent
* Fix warnings
* Refactor compressVideo to use async/await
Updated the compressVideo function to use Swift's async/await syntax instead of withCheckedContinuation, simplifying the code and improving readability.
* Rename SPM Network -> NetworkClient
* Fix build
* Client -> MastodonClient
* Rename file
* Refactor media container to use state-based model
Replaces the previous MediaContainer property model with a state-based enum to better represent the lifecycle of media (pending, uploading, uploaded, failed). Updates MediaView and ViewModel to use the new state model, improving clarity and error handling for media uploads, progress, and failures. Adds convenience initializers and factory methods for creating containers in various states, and updates UI logic to match the new structure.
* Add media upload progress tracking and UI updates
Introduces progress tracking for media uploads in MastodonClient by adding new upload methods with progress handlers and a URLSession delegate. Updates StatusEditor ViewModel to pass progress handlers and update media container state during uploads. Enhances MediaView to display both circular and linear progress indicators with animation.
* Refactor message view to use glass effect on iOS 26+
Introduces a conditional to use .glassEffect for message backgrounds on iOS 26 and above, while maintaining the previous background logic for earlier versions. Extracts the message text rendering into a reusable textView property for cleaner code.
* Test removing legacy app icon
* Revert "Test removing legacy app icon"
This reverts commit 27c552d3cb
.
* Refactor timeline pills and tab bar for iOS 26 compatibility
Moved TimelineQuickAccessPills logic from AppView to TimelineView and updated implementation to use new iOS 26 APIs where available. Simplified tab bar view in AppView and improved conditional logic for iPad and Mac layouts.
* Update glassEffect to use .regular.interactive()
Replaces the default glassEffect modifier with .regular.interactive() for improved visual consistency and interaction feedback on FollowButton and related controls.
* Improve progress indicator logic and remove iOS version check
Refines the display logic for progress indicators in MediaView, showing a linear progress bar only when progress is between 0 and 1, and otherwise showing a circular indicator. Also removes an unnecessary iOS version check in TimelineView for toolbar background visibility.
* Update screenshots
* Update toolbar tint and improve account fields UI
Added `.tint(.label)` to several toolbar items for consistent icon coloring. Refactored AccountFieldsView to support iOS 26+ visual effects and improved accessibility and background handling. Removed redundant `.tint(.label)` from NotificationsListView.
* Add zoom transition for MediaUI
* Remove Old AppIcon
* Remove alternate icons assets
* Update sign-in button style and layout in AddAccountView
Refactors the sign-in button to use a new 'signinButton' view, applying .glassProminent style on iOS 26+ and .borderedProminent otherwise. Adjusts button layout for full width, sets a fixed height, and updates row insets and background for improved appearance.
* Remove iPhone tab label preference setting
Eliminated the 'showiPhoneTabLabel' toggle from TabbarEntriesSettingsView and related property from UserPreferences. This streamlines settings by removing an unused or deprecated option.
* Fix text replacement in StatusEditor ViewModel
Updated the async stream handling in StatusEditor to use the 'content' property of the streamed object when replacing text, ensuring correct text updates.
* Update UI for iOS 26 and improve avatar effects
Refactored ToolbarTab and AppAccountsSelectorView to use .embed avatarConfig and apply glassEffect on iOS 26+. Updated contentShape to .circle for better interaction. Added theme-based tint to notification list buttons for improved visual consistency.
* Add optional caption to Mastodon post intent (#2292)
* Add app shortcut and intent for inline image posting on Mastodon (#2293)
* Add optional alt text to image upload intents (#2294)
* Adjust toolbar label offset for iOS 26 (#2296)
* Improve background handling in Explore and Editor views
Updated ExploreView to use edgesIgnoringSafeArea for the background color and made minor formatting improvements. Refactored MainView in StatusEditor to use a computed backgroundColor view for better background management based on presentationDetent.
* Remove macCatalyst conditional compilation for AI features
Eliminated #if !targetEnvironment(macCatalyst) checks from AI-related components in the status editor, making AI features available on all platforms where iOS 26+ is supported. Also added new localization keys related to image posting and descriptions.
* Add ToolbarSpacer and improve toolbar styling
Introduces ToolbarSpacer with .topBarTrailing placement for iOS 26.0+ in NotificationTab, TimelineTab, and TimelineView to improve toolbar layout. Updates toolbar item placements and adds theme-based foreground styling to toolbar icons. Removes unnecessary line limit in NotificationRowContentView and adjusts line limit logic in StatusRowTextView for better text display. Refactors some pattern matching for clarity.
BIN
AlternateIcons/AppIconAlternate1.icon/Assets/puple_cube.png
Normal file
After Width: | Height: | Size: 237 KiB |
163
AlternateIcons/AppIconAlternate1.icon/icon.json
Normal file
|
@ -0,0 +1,163 @@
|
|||
{
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"linear-gradient" : [
|
||||
"display-p3:0.36471,0.36863,0.94510,1.00000",
|
||||
"srgb:0.57919,0.12801,0.57269,1.00000"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : "system-dark"
|
||||
}
|
||||
],
|
||||
"groups" : [
|
||||
{
|
||||
"blend-mode-specializations" : [
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : "normal"
|
||||
}
|
||||
],
|
||||
"blur-material" : null,
|
||||
"layers" : [
|
||||
{
|
||||
"blend-mode" : "screen",
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"solid" : "srgb:1.00000,1.00000,1.00000,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : "automatic"
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"linear-gradient" : [
|
||||
"display-p3:0.90000,0.90000,0.90000,0.83000",
|
||||
"srgb:1.00000,1.00000,1.00000,0.41987"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"glass" : true,
|
||||
"hidden" : false,
|
||||
"image-name" : "puple_cube.png",
|
||||
"name" : "puple_cube",
|
||||
"position" : {
|
||||
"scale" : 1.24,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"lighting" : "individual",
|
||||
"shadow" : {
|
||||
"kind" : "layer-color",
|
||||
"opacity" : 0.59
|
||||
},
|
||||
"specular" : true,
|
||||
"translucency-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.87
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"enabled" : false,
|
||||
"value" : 0.84
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"blur-material-specializations" : [
|
||||
{
|
||||
"value" : 1
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : 1
|
||||
}
|
||||
],
|
||||
"layers" : [
|
||||
{
|
||||
"blend-mode-specializations" : [
|
||||
{
|
||||
"value" : "screen"
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : "lighten"
|
||||
}
|
||||
],
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"solid" : "srgb:1.00000,0.25279,1.00000,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : {
|
||||
"solid" : "display-p3:0.82510,0.25332,1.00000,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : "automatic"
|
||||
}
|
||||
],
|
||||
"glass" : true,
|
||||
"hidden" : false,
|
||||
"image-name" : "puple_cube.png",
|
||||
"name" : "puple_cube",
|
||||
"position" : {
|
||||
"scale" : 1.13,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"lighting" : "individual",
|
||||
"shadow" : {
|
||||
"kind" : "layer-color",
|
||||
"opacity" : 1
|
||||
},
|
||||
"specular" : true,
|
||||
"translucency-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.3
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"enabled" : false,
|
||||
"value" : 0.1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"supported-platforms" : {
|
||||
"circles" : [
|
||||
"watchOS"
|
||||
],
|
||||
"squares" : "shared"
|
||||
}
|
||||
}
|
5
AlternateIcons/AppIconAlternate2.icon/Assets/Oval 1.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 3.6.18 -->
|
||||
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Oval" fill="#b082ff" fill-rule="evenodd" stroke="none" d="M 50 25 C 50 11.192883 38.807117 0 25 0 C 11.192882 0 0 11.192883 0 25 C 0 38.807117 11.192882 50 25 50 C 38.807117 50 50 38.807117 50 25 Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 389 B |
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 3.6.18 -->
|
||||
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Oval-copy" fill="#b082ff" fill-rule="evenodd" stroke="none" d="M 50 25 C 50 11.192883 38.807117 0 25 0 C 11.192882 0 0 11.192883 0 25 C 0 38.807117 11.192882 50 25 50 C 38.807117 50 50 38.807117 50 25 Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 394 B |
5
AlternateIcons/AppIconAlternate2.icon/Assets/Oval.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 3.6.18 -->
|
||||
<svg width="563" height="116" viewBox="0 0 563 116" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Oval" fill="#9368ef" fill-rule="evenodd" stroke="none" d="M 563 58 C 563 25.967484 436.96817 0 281.5 0 C 126.031845 0 0 25.967484 0 58 C 0 90.032516 126.031845 116 281.5 116 C 436.96817 116 563 90.032516 563 58 Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 408 B |
5
AlternateIcons/AppIconAlternate2.icon/Assets/Path 1.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 3.6.18 -->
|
||||
<svg width="507" height="394" viewBox="0 0 507 394" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Path" fill="#b082ff" fill-rule="evenodd" stroke="none" d="M 483 2 C 483 2 512.988586 13.092346 497 41 C 482.658203 66.033203 438.230896 111.900574 459 139 C 473.848694 158.374512 473.556641 143.664063 497 154 C 501.080017 155.798828 511.591797 163.125 503 179 C 494.266113 195.137512 476.132813 205.480469 472 237 C 467.867188 268.519531 497 250 497 250 L 489 320 C 489 320 481.453125 293.089844 402 348 C 322.546875 402.910156 262.179321 377.661072 251 375 C 175.819977 357.104492 166.122375 354.23291 160 362 C 128.982666 401.349884 64.947479 401.921997 3 376 C 1.566711 375.400238 1.434143 370.590149 1 365 C -1.16745 337.091187 142.118988 331.994934 222 250 C 318.242188 151.210938 280.920471 115.197754 306 90 C 321.328125 74.599609 316.399841 51.071167 414 57 C 425.937134 57.725159 444.693237 28.917725 455 15 C 470.195007 -5.518616 483 2 483 2 Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1 KiB |
5
AlternateIcons/AppIconAlternate2.icon/Assets/Path 2.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 3.6.18 -->
|
||||
<svg width="320" height="44" viewBox="0 0 320 44" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Path" fill="#9368ef" fill-opacity="0.397493" fill-rule="evenodd" stroke="none" d="M 15 20 C 12.371399 19.847656 0.616394 39.961365 1 40 C 61.304993 46.071472 224.469727 45.789246 319 15 C 321.072937 14.324829 314.229187 13.876343 311 9 C 309.598389 6.883423 312.004028 0.24054 309 1 C 224.475891 22.368103 81.76416 23.870117 15 20 Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 526 B |
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 3.6.18 -->
|
||||
<svg width="487" height="349" viewBox="0 0 487 349" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Path-copy-2" fill="#4c2cb2" fill-rule="evenodd" stroke="none" d="M 456 260 C 456.742737 256.67865 487 7 487 7 C 487 7 479.453125 -19.910156 400 35 C 320.546875 89.910156 260.179321 64.661072 249 62 C 173.819977 44.104492 164.122375 41.23291 158 49 C 126.982666 88.349884 62.947479 88.921997 1 63 C -0.433289 62.400238 19.681335 249.13916 20 250 C 45.552124 319.030579 96.464447 340.619385 228 348 C 335.737 354.045258 442.191467 321.749542 456 260 Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 645 B |
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 3.6.18 -->
|
||||
<svg width="510" height="406" viewBox="0 0 510 406" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Path-copy" fill="#5737b5" fill-opacity="0.18601" fill-rule="evenodd" stroke="none" d="M 272 35 C 211.596466 34.858887 72.501373 32.551025 1 2 C 0.651367 1.851013 35.419159 312.718201 36 315 C 50.539032 372.116516 111.354187 404.779663 233 405 C 256.668945 405.042847 304 405 304 405 C 304 405 461.608398 395.958496 472 322 C 480.747559 259.742432 509.830811 0.628601 509 1 C 446.60675 28.893738 322.028076 35.116882 272 35 Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 620 B |
5
AlternateIcons/AppIconAlternate2.icon/Assets/Path.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 3.6.18 -->
|
||||
<svg width="564" height="503" viewBox="0 0 564 503" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Path" fill="#e3d9fa" fill-opacity="0.764026" fill-rule="evenodd" stroke="none" d="M 1 2 C 6.978516 13.994141 27 25 27 25 C 27 25 61.419159 335.718201 62 338 C 76.539032 395.116516 137.354187 427.779663 259 428 C 282.668945 428.042847 330 428 330 428 C 330 428 487.608398 418.958496 498 345 C 506.747559 282.742432 536 24 536 24 C 536 24 555.78833 10.634644 563 1 C 566.011047 -3.022705 515 416 515 416 C 515 416 520.245361 450.830627 472 472 C 424.911926 492.66156 304.897827 514.033081 174 495 C 25.14917 473.356506 46.727173 419.739075 43 388 C 30.527893 281.792633 -4.978516 -9.994141 1 2 Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 789 B |
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 3.6.18 -->
|
||||
<svg width="248" height="245" viewBox="0 0 248 245" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Rounded-Rectangle-copy-2" fill="#ffffff" fill-rule="evenodd" stroke="none" d="M 6.537902 110.999268 C -4.468609 126.213013 -1.057972 147.468735 14.155773 158.47525 L 123.515793 237.592667 C 138.729538 248.599167 159.985245 245.188538 170.99176 229.974792 L 241.043701 133.145523 C 252.050217 117.931778 248.639572 96.676056 233.425842 85.669556 L 124.065819 6.552124 C 108.852074 -4.454376 87.596359 -1.043747 76.589844 14.169998 Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 627 B |
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 3.6.18 -->
|
||||
<svg width="248" height="250" viewBox="0 0 248 250" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Rounded-Rectangle-copy" fill="#ffffff" fill-rule="evenodd" stroke="none" d="M 11.10811 90.739502 C -2.717603 103.445831 -3.625039 124.954315 9.081296 138.780029 L 100.417458 238.162537 C 113.123787 251.988251 134.632263 252.895691 148.457977 240.189362 L 236.453003 159.318726 C 250.278717 146.612396 251.186142 125.103912 238.479813 111.278198 L 147.143661 11.895691 C 134.437317 -1.930023 112.928841 -2.837463 99.103127 9.868866 Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 628 B |
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 3.6.18 -->
|
||||
<svg width="248" height="245" viewBox="0 0 248 245" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Rounded-Rectangle" fill="#ffffff" fill-rule="evenodd" stroke="none" d="M 75.916382 229.732666 C 86.797638 245.03624 108.024643 248.621246 123.328224 237.73999 L 233.334015 159.522949 C 248.637589 148.641693 252.222595 127.414696 241.341339 112.111115 L 172.086609 14.710068 C 161.205353 -0.593521 139.978348 -4.178528 124.674767 6.702728 L 14.668981 84.919769 C -0.634602 95.801025 -4.219606 117.028038 6.661647 132.331604 Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 620 B |
5
AlternateIcons/AppIconAlternate2.icon/Assets/Shape.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 3.6.18 -->
|
||||
<svg width="509" height="73" viewBox="0 0 509 73" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Shape" fill="#b082ff" fill-rule="evenodd" stroke="none" d="M 509 36.5 C 509 16.341614 380.556458 0 240 0 C 99.443542 0 0 16.341614 0 36.5 C 0 53.433044 80.39856 67.672974 189.434479 71.796265 C 210.203217 72.581665 232.010956 73 254.5 73 C 395.056458 73 509 56.658386 509 36.5 Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 472 B |
217
AlternateIcons/AppIconAlternate2.icon/icon.json
Normal file
|
@ -0,0 +1,217 @@
|
|||
{
|
||||
"fill" : {
|
||||
"linear-gradient" : [
|
||||
"display-p3:0.49338,0.09624,0.75589,1.00000",
|
||||
"display-p3:0.10553,0.00332,0.25763,1.00000"
|
||||
]
|
||||
},
|
||||
"groups" : [
|
||||
{
|
||||
"layers" : [
|
||||
{
|
||||
"image-name" : "Path 2.svg",
|
||||
"name" : "Path 2",
|
||||
"position" : {
|
||||
"scale" : 1,
|
||||
"translation-in-points" : [
|
||||
78,
|
||||
-211
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"glass" : false,
|
||||
"image-name" : "Path copy.svg",
|
||||
"name" : "Path copy",
|
||||
"position" : {
|
||||
"scale" : 1,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
-23
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"blend-mode-specializations" : [
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : "normal"
|
||||
}
|
||||
],
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : "none"
|
||||
}
|
||||
],
|
||||
"image-name" : "Rounded Rectangle copy 2.svg",
|
||||
"name" : "Rounded Rectangle copy 2",
|
||||
"opacity-specializations" : [
|
||||
{
|
||||
"value" : 0.9
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : 0.8
|
||||
}
|
||||
],
|
||||
"position" : {
|
||||
"scale" : 1,
|
||||
"translation-in-points" : [
|
||||
39,
|
||||
-186.1796875
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : "none"
|
||||
}
|
||||
],
|
||||
"image-name" : "Rounded Rectangle.svg",
|
||||
"name" : "Rounded Rectangle",
|
||||
"opacity-specializations" : [
|
||||
{
|
||||
"value" : 0.9
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : 0.8
|
||||
}
|
||||
],
|
||||
"position" : {
|
||||
"scale" : 1,
|
||||
"translation-in-points" : [
|
||||
-111,
|
||||
-35
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : "none"
|
||||
}
|
||||
],
|
||||
"image-name" : "Rounded Rectangle copy.svg",
|
||||
"name" : "Rounded Rectangle copy",
|
||||
"opacity-specializations" : [
|
||||
{
|
||||
"value" : 0.9
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : 0.8
|
||||
}
|
||||
],
|
||||
"position" : {
|
||||
"scale" : 1,
|
||||
"translation-in-points" : [
|
||||
96.51953125,
|
||||
48
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"image-name" : "Oval 1.svg",
|
||||
"name" : "Oval 1",
|
||||
"position" : {
|
||||
"scale" : 1,
|
||||
"translation-in-points" : [
|
||||
-47.87109375,
|
||||
-342.03125
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"image-name" : "Oval copy.svg",
|
||||
"name" : "Oval copy",
|
||||
"opacity" : 1,
|
||||
"position" : {
|
||||
"scale" : 1,
|
||||
"translation-in-points" : [
|
||||
278.50390625,
|
||||
-375.5703125
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"image-name" : "Path.svg",
|
||||
"name" : "Path"
|
||||
},
|
||||
{
|
||||
"image-name" : "Path 1.svg",
|
||||
"name" : "Path 1",
|
||||
"position" : {
|
||||
"scale" : 1,
|
||||
"translation-in-points" : [
|
||||
10,
|
||||
-277
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"glass" : false,
|
||||
"hidden" : false,
|
||||
"image-name" : "Shape.svg",
|
||||
"name" : "Shape",
|
||||
"position" : {
|
||||
"scale" : 1,
|
||||
"translation-in-points" : [
|
||||
1.216796875,
|
||||
-248
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"hidden" : false,
|
||||
"image-name" : "Oval.svg",
|
||||
"name" : "Oval",
|
||||
"position" : {
|
||||
"scale" : 1,
|
||||
"translation-in-points" : [
|
||||
-0.62109375,
|
||||
-247.6171875
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"glass" : false,
|
||||
"image-name" : "Path copy 2.svg",
|
||||
"name" : "Path copy 2",
|
||||
"position" : {
|
||||
"scale" : 1.1,
|
||||
"translation-in-points" : [
|
||||
2,
|
||||
10
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"position" : {
|
||||
"scale" : 1,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
97.5
|
||||
]
|
||||
},
|
||||
"shadow" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"translucency" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.5
|
||||
}
|
||||
}
|
||||
],
|
||||
"supported-platforms" : {
|
||||
"circles" : [
|
||||
"watchOS"
|
||||
],
|
||||
"squares" : "shared"
|
||||
}
|
||||
}
|
BIN
AlternateIcons/AppIconAlternate46.icon/Assets/puple_cube.png
Normal file
After Width: | Height: | Size: 237 KiB |
309
AlternateIcons/AppIconAlternate46.icon/icon.json
Normal file
|
@ -0,0 +1,309 @@
|
|||
{
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"linear-gradient" : [
|
||||
"display-p3:0.67059,0.26667,0.85098,1.00000",
|
||||
"display-p3:0.11373,0.12941,0.22353,1.00000"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : "system-dark"
|
||||
}
|
||||
],
|
||||
"groups" : [
|
||||
{
|
||||
"blend-mode-specializations" : [
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : "normal"
|
||||
}
|
||||
],
|
||||
"blur-material" : 1,
|
||||
"layers" : [
|
||||
{
|
||||
"blend-mode" : "screen",
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"solid" : "srgb:1.00000,1.00000,1.00000,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : "automatic"
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"linear-gradient" : [
|
||||
"display-p3:0.90000,0.90000,0.90000,0.83000",
|
||||
"srgb:1.00000,1.00000,1.00000,0.41987"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"glass" : true,
|
||||
"hidden" : false,
|
||||
"image-name" : "puple_cube.png",
|
||||
"name" : "puple_cube",
|
||||
"position" : {
|
||||
"scale" : 1.24,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"lighting" : "individual",
|
||||
"shadow" : {
|
||||
"kind" : "layer-color",
|
||||
"opacity" : 1
|
||||
},
|
||||
"specular" : true,
|
||||
"translucency-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.2
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"blur-material-specializations" : [
|
||||
{
|
||||
"value" : 1
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : 1
|
||||
}
|
||||
],
|
||||
"layers" : [
|
||||
{
|
||||
"blend-mode-specializations" : [
|
||||
{
|
||||
"value" : "screen"
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : "lighten"
|
||||
}
|
||||
],
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"solid" : "srgb:1.00000,0.25279,1.00000,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : {
|
||||
"solid" : "display-p3:0.82510,0.25332,1.00000,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : "automatic"
|
||||
}
|
||||
],
|
||||
"glass" : true,
|
||||
"hidden" : false,
|
||||
"image-name" : "puple_cube.png",
|
||||
"name" : "puple_cube",
|
||||
"position" : {
|
||||
"scale" : 1.13,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"lighting" : "individual",
|
||||
"shadow" : {
|
||||
"kind" : "layer-color",
|
||||
"opacity" : 1
|
||||
},
|
||||
"specular" : true,
|
||||
"translucency-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.3
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"blur-material-specializations" : [
|
||||
{
|
||||
"value" : 1
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : 1
|
||||
}
|
||||
],
|
||||
"layers" : [
|
||||
{
|
||||
"blend-mode-specializations" : [
|
||||
{
|
||||
"value" : "screen"
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : "lighten"
|
||||
}
|
||||
],
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"solid" : "display-p3:0.96921,0.39989,0.04269,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : {
|
||||
"solid" : "display-p3:1.00000,0.61199,0.29857,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : "automatic"
|
||||
}
|
||||
],
|
||||
"glass" : true,
|
||||
"hidden" : false,
|
||||
"image-name" : "puple_cube.png",
|
||||
"name" : "puple_cube",
|
||||
"position" : {
|
||||
"scale" : 1.02,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"lighting" : "individual",
|
||||
"shadow" : {
|
||||
"kind" : "layer-color",
|
||||
"opacity" : 1
|
||||
},
|
||||
"specular" : true,
|
||||
"translucency-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.3
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.5
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"blur-material-specializations" : [
|
||||
{
|
||||
"value" : 1
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : 1
|
||||
}
|
||||
],
|
||||
"layers" : [
|
||||
{
|
||||
"blend-mode-specializations" : [
|
||||
{
|
||||
"value" : "screen"
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : "lighten"
|
||||
}
|
||||
],
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"solid" : "display-p3:0.96921,0.94842,0.00650,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : {
|
||||
"solid" : "display-p3:1.00000,0.89360,0.20100,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : "automatic"
|
||||
}
|
||||
],
|
||||
"glass" : true,
|
||||
"hidden" : false,
|
||||
"image-name" : "puple_cube.png",
|
||||
"name" : "puple_cube",
|
||||
"position" : {
|
||||
"scale" : 0.92,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"lighting" : "individual",
|
||||
"shadow" : {
|
||||
"kind" : "layer-color",
|
||||
"opacity" : 1
|
||||
},
|
||||
"specular" : true,
|
||||
"translucency-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.3
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.7
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"supported-platforms" : {
|
||||
"circles" : [
|
||||
"watchOS"
|
||||
],
|
||||
"squares" : "shared"
|
||||
}
|
||||
}
|
BIN
AppIcon.icon/Assets/puple_cube.png
Normal file
After Width: | Height: | Size: 237 KiB |
149
AppIcon.icon/icon.json
Normal file
|
@ -0,0 +1,149 @@
|
|||
{
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : "system-light"
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : "system-dark"
|
||||
}
|
||||
],
|
||||
"groups" : [
|
||||
{
|
||||
"blend-mode-specializations" : [
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : "normal"
|
||||
}
|
||||
],
|
||||
"blur-material" : null,
|
||||
"layers" : [
|
||||
{
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : "automatic"
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"linear-gradient" : [
|
||||
"display-p3:0.90000,0.90000,0.90000,0.83000",
|
||||
"srgb:1.00000,1.00000,1.00000,0.41987"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"glass" : true,
|
||||
"hidden" : false,
|
||||
"image-name" : "puple_cube.png",
|
||||
"name" : "puple_cube",
|
||||
"position" : {
|
||||
"scale" : 1.24,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"lighting" : "individual",
|
||||
"shadow" : {
|
||||
"kind" : "layer-color",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"specular" : true,
|
||||
"translucency-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.84
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"enabled" : false,
|
||||
"value" : 0.84
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"blur-material-specializations" : [
|
||||
{
|
||||
"value" : 1
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : 1
|
||||
}
|
||||
],
|
||||
"layers" : [
|
||||
{
|
||||
"blend-mode-specializations" : [
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : "lighten"
|
||||
}
|
||||
],
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"solid" : "srgb:1.00000,0.25279,1.00000,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : {
|
||||
"solid" : "display-p3:0.82510,0.25332,1.00000,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : "automatic"
|
||||
}
|
||||
],
|
||||
"glass" : true,
|
||||
"hidden" : false,
|
||||
"image-name" : "puple_cube.png",
|
||||
"name" : "puple_cube",
|
||||
"position" : {
|
||||
"scale" : 1.13,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"lighting" : "individual",
|
||||
"shadow" : {
|
||||
"kind" : "layer-color",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"specular" : true,
|
||||
"translucency-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.1
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"enabled" : false,
|
||||
"value" : 0.1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"supported-platforms" : {
|
||||
"circles" : [
|
||||
"watchOS"
|
||||
],
|
||||
"squares" : "shared"
|
||||
}
|
||||
}
|
283
CLAUDE.md
|
@ -8,19 +8,12 @@ IceCubesApp is a multiplatform Mastodon client built entirely in SwiftUI. It's a
|
|||
|
||||
## Build Commands
|
||||
|
||||
### Initial Setup
|
||||
1. Clone the repository
|
||||
2. Create your configuration file:
|
||||
### Building for iOS Simulator
|
||||
To build IceCubesApp for iPhone 16 Pro simulator:
|
||||
```bash
|
||||
cp IceCubesApp.xcconfig.template IceCubesApp.xcconfig
|
||||
mcp__XcodeBuildMCP__build_sim_name_proj projectPath: "/Users/thomas/Documents/Dev/Open Source/IceCubesApp/IceCubesApp.xcodeproj" scheme: "IceCubesApp" simulatorName: "iPhone 16 Pro"
|
||||
```
|
||||
3. Edit `IceCubesApp.xcconfig` and add:
|
||||
- `DEVELOPMENT_TEAM` = Your Apple Developer Team ID
|
||||
- `BUNDLE_ID_PREFIX` = Your bundle identifier prefix (e.g., com.yourcompany)
|
||||
|
||||
### Building
|
||||
- **Xcode GUI**: Open `IceCubesApp.xcodeproj` and build
|
||||
- **Command Line**: `xcodebuild -scheme IceCubesApp build`
|
||||
|
||||
### Running Tests
|
||||
- **All tests**: Run through Xcode's Test navigator
|
||||
|
@ -80,51 +73,90 @@ The codebase contains legacy MVVM patterns, but **new features should NOT use Vi
|
|||
|
||||
## Modern SwiftUI Architecture Guidelines (2025)
|
||||
|
||||
### No ViewModels - Use Native SwiftUI Data Flow
|
||||
**New features MUST follow these patterns:**
|
||||
### Core Philosophy
|
||||
|
||||
1. **Views as Pure State Expressions**
|
||||
- SwiftUI is the default UI paradigm - embrace its declarative nature
|
||||
- Avoid legacy UIKit patterns and unnecessary abstractions
|
||||
- Focus on simplicity, clarity, and native data flow
|
||||
- Let SwiftUI handle the complexity - don't fight the framework
|
||||
- **No ViewModels** - Use native SwiftUI data flow patterns
|
||||
|
||||
### Architecture Principles
|
||||
|
||||
#### 1. Native State Management
|
||||
|
||||
Use SwiftUI's built-in property wrappers appropriately:
|
||||
- `@State` - Local, ephemeral view state
|
||||
- `@Binding` - Two-way data flow between views
|
||||
- `@Observable` - Shared state (preferred for new code)
|
||||
- `@Environment` - Dependency injection for app-wide concerns
|
||||
|
||||
#### 2. State Ownership
|
||||
|
||||
- Views own their local state unless sharing is required
|
||||
- State flows down, actions flow up
|
||||
- Keep state as close to where it's used as possible
|
||||
- Extract shared state only when multiple views need it
|
||||
|
||||
Example:
|
||||
```swift
|
||||
struct MyView: View {
|
||||
@Environment(MyService.self) private var service
|
||||
struct TimelineView: View {
|
||||
@Environment(Client.self) private var client
|
||||
@State private var viewState: ViewState = .loading
|
||||
|
||||
enum ViewState {
|
||||
case loading
|
||||
case loaded(data: [Item])
|
||||
case error(String)
|
||||
case loaded(statuses: [Status])
|
||||
case error(Error)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
// View is just a representation of its state
|
||||
Group {
|
||||
switch viewState {
|
||||
case .loading:
|
||||
ProgressView()
|
||||
case .loaded(let statuses):
|
||||
StatusList(statuses: statuses)
|
||||
case .error(let error):
|
||||
ErrorView(error: error)
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await loadTimeline()
|
||||
}
|
||||
}
|
||||
|
||||
private func loadTimeline() async {
|
||||
do {
|
||||
let statuses = try await client.getHomeTimeline()
|
||||
viewState = .loaded(statuses: statuses)
|
||||
} catch {
|
||||
viewState = .error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Use Environment Appropriately**
|
||||
- **App-wide services**: Router, Theme, CurrentAccount, Client, etc. - use `@Environment`
|
||||
- **Feature-specific services**: Timeline services, single-view logic - use `let` properties with `@Observable`
|
||||
- Rule: Environment for cross-app/cross-feature dependencies, let properties for single-feature services
|
||||
- Access app-wide via `@Environment(ServiceType.self)`
|
||||
- Feature services: `private let myService = MyObservableService()`
|
||||
#### 3. Modern Async Patterns
|
||||
|
||||
3. **Local State Management**
|
||||
- Use `@State` for view-specific state
|
||||
- Use `enum` for view states (loading, loaded, error)
|
||||
- Use `.task(id:)` and `.onChange(of:)` for side effects
|
||||
- Pass state between views using `@Binding`
|
||||
- Use `async/await` as the default for asynchronous operations
|
||||
- Leverage `.task` modifier for lifecycle-aware async work
|
||||
- Handle errors gracefully with try/catch
|
||||
- Avoid Combine unless absolutely necessary
|
||||
|
||||
4. **No ViewModels Required**
|
||||
- Views should be lightweight and disposable
|
||||
- Business logic belongs in services/clients
|
||||
- Test services independently, not views
|
||||
- Use SwiftUI previews for visual testing
|
||||
#### 4. View Composition
|
||||
|
||||
5. **When Views Get Complex**
|
||||
- Split into smaller subviews
|
||||
- Use compound views that compose smaller views
|
||||
- Pass state via bindings between views
|
||||
- Never reach for a ViewModel as the solution
|
||||
- Build UI with small, focused views
|
||||
- Extract reusable components naturally
|
||||
- Use view modifiers to encapsulate common styling
|
||||
- Prefer composition over inheritance
|
||||
|
||||
#### 5. Code Organization
|
||||
|
||||
- Organize by feature (e.g., Timeline/, Account/, Settings/)
|
||||
- Keep related code together in the same file when appropriate
|
||||
- Use extensions to organize large files
|
||||
- Follow Swift naming conventions consistently
|
||||
|
||||
### Build Verification Process
|
||||
**IMPORTANT**: When editing code, you MUST:
|
||||
|
@ -132,6 +164,7 @@ The codebase contains legacy MVVM patterns, but **new features should NOT use Vi
|
|||
1. Build the project after making changes using XcodeBuildMCP commands
|
||||
2. Fix any compilation errors before proceeding
|
||||
3. Run relevant tests if modifying existing functionality
|
||||
4. Ensure code follows modern SwiftUI patterns
|
||||
|
||||
Example workflow:
|
||||
```bash
|
||||
|
@ -142,15 +175,183 @@ mcp__XcodeBuildMCP__build_mac_proj projectPath: "/path/to/IceCubesApp.xcodeproj"
|
|||
mcp__XcodeBuildMCP__build_ios_sim_name_proj projectPath: "/path/to/IceCubesApp.xcodeproj" scheme: "IceCubesApp" simulatorName: "iPhone 16"
|
||||
```
|
||||
|
||||
### Implementation Examples
|
||||
|
||||
#### Shared State with @Observable
|
||||
```swift
|
||||
@Observable
|
||||
class AppAccountsManager {
|
||||
var currentAccount: Account?
|
||||
var availableAccounts: [Account] = []
|
||||
|
||||
func switchAccount(_ account: Account) {
|
||||
currentAccount = account
|
||||
// Handle account switching
|
||||
}
|
||||
}
|
||||
|
||||
// In App file
|
||||
struct IceCubesApp: App {
|
||||
@State private var accountManager = AppAccountsManager()
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.environment(accountManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Modern Async Data Loading
|
||||
```swift
|
||||
struct NotificationsView: View {
|
||||
@Environment(Client.self) private var client
|
||||
@State private var notifications: [Notification] = []
|
||||
@State private var isLoading = false
|
||||
@State private var error: Error?
|
||||
|
||||
var body: some View {
|
||||
List(notifications) { notification in
|
||||
NotificationRow(notification: notification)
|
||||
}
|
||||
.overlay {
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await loadNotifications()
|
||||
}
|
||||
.refreshable {
|
||||
await loadNotifications()
|
||||
}
|
||||
}
|
||||
|
||||
private func loadNotifications() async {
|
||||
isLoading = true
|
||||
defer { isLoading = false }
|
||||
|
||||
do {
|
||||
notifications = try await client.getNotifications()
|
||||
} catch {
|
||||
self.error = error
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
#### DO:
|
||||
- Write self-contained views when possible
|
||||
- Use property wrappers as intended by Apple
|
||||
- Test logic in isolation, preview UI visually
|
||||
- Handle loading and error states explicitly
|
||||
- Keep views focused on presentation
|
||||
- Use Swift's type system for safety
|
||||
- Trust SwiftUI's update mechanism
|
||||
|
||||
#### DON'T:
|
||||
- Create ViewModels for every view
|
||||
- Move state out of views unnecessarily
|
||||
- Add abstraction layers without clear benefit
|
||||
- Use Combine for simple async operations
|
||||
- Fight SwiftUI's update mechanism
|
||||
- Overcomplicate simple features
|
||||
- **Nest @Observable objects within other @Observable objects** - This breaks SwiftUI's observation system. Initialize services at the view level instead.
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
- Unit test business logic in services/clients
|
||||
- Use SwiftUI Previews for visual testing
|
||||
- Test @Observable classes independently
|
||||
- Keep tests simple and focused
|
||||
- Don't sacrifice code clarity for testability
|
||||
|
||||
### Code Style When Editing
|
||||
- Maintain existing patterns in legacy code
|
||||
- New features use modern patterns exclusively
|
||||
- Prefer composition over inheritance
|
||||
- Keep views focused and single-purpose
|
||||
- Use descriptive names for state enums
|
||||
- Write SwiftUI code that looks and feels like SwiftUI
|
||||
|
||||
## Development Requirements
|
||||
- Minimum Swift 6.0
|
||||
- Minimum deployment: iOS 17.0, visionOS 1.0
|
||||
- Xcode 15.0 or later
|
||||
- iOS 26 SDK (June 2025)
|
||||
- Minimum deployment: iOS 18.0, visionOS 1.0
|
||||
- Xcode 16.0 or later with iOS 26 SDK
|
||||
- Apple Developer account for device testing
|
||||
|
||||
## iOS 26 SDK Integration
|
||||
|
||||
**IMPORTANT**: The project now supports iOS 26 SDK (June 2025) while maintaining iOS 18 as the minimum deployment target. Use `#available` checks when adopting iOS 26+ APIs.
|
||||
|
||||
### Available iOS 26 SwiftUI APIs
|
||||
|
||||
#### Liquid Glass Effects
|
||||
- `glassEffect(_:in:isEnabled:)` - Apply Liquid Glass effects to views
|
||||
- `buttonStyle(.glass)` - Apply Liquid Glass styling to buttons
|
||||
- `ToolbarSpacer` - Create visual breaks in toolbars with Liquid Glass
|
||||
|
||||
Example:
|
||||
```swift
|
||||
Button("Post", action: postStatus)
|
||||
.buttonStyle(.glass)
|
||||
.glassEffect(.thin, in: .rect(cornerRadius: 12))
|
||||
```
|
||||
|
||||
#### Enhanced Scrolling
|
||||
- `scrollEdgeEffectStyle(_:for:)` - Configure scroll edge effects
|
||||
- `backgroundExtensionEffect()` - Duplicate, mirror, and blur views around edges
|
||||
|
||||
#### Tab Bar Enhancements
|
||||
- `tabBarMinimizeBehavior(_:)` - Control tab bar minimization behavior
|
||||
- Search role for tabs with search field replacing tab bar
|
||||
- `TabViewBottomAccessoryPlacement` - Adjust accessory view content based on placement
|
||||
|
||||
#### Web Integration
|
||||
- `WebView` and `WebPage` - Full control over browsing experience
|
||||
|
||||
#### Drag and Drop
|
||||
- `draggable(_:_:)` - Drag multiple items
|
||||
- `dragContainer(for:id:in:selection:_:)` - Container for draggable views
|
||||
|
||||
#### Animation
|
||||
- `@Animatable` macro - SwiftUI synthesizes custom animatable data properties
|
||||
|
||||
#### UI Components
|
||||
- `Slider` with automatic tick marks when using step parameter
|
||||
- `windowResizeAnchor(_:)` - Set window anchor point for resizing
|
||||
|
||||
#### Text Enhancements
|
||||
- `TextEditor` now supports `AttributedString`
|
||||
- `AttributedTextSelection` - Handle text selection with attributed text
|
||||
- `AttributedTextFormattingDefinition` - Define text styling in specific contexts
|
||||
- `FindContext` - Create find navigator in text editing views
|
||||
|
||||
#### Accessibility
|
||||
- `AssistiveAccess` - Support Assistive Access in iOS/iPadOS scenes
|
||||
|
||||
#### HDR Support
|
||||
- `Color.ResolvedHDR` - RGBA values with HDR headroom information
|
||||
|
||||
#### UIKit Integration
|
||||
- `UIHostingSceneDelegate` - Host and present SwiftUI scenes in UIKit
|
||||
- `NSHostingSceneRepresentation` - Host SwiftUI scenes in AppKit
|
||||
- `NSGestureRecognizerRepresentable` - Incorporate gesture recognizers from AppKit
|
||||
|
||||
#### Immersive Spaces (visionOS)
|
||||
- `manipulable(coordinateSpace:operations:inertia:isEnabled:onChanged:)` - Hand gesture manipulation
|
||||
- `SurfaceSnappingInfo` - Snap volumes and windows to surfaces
|
||||
- `RemoteImmersiveSpace` - Render stereo content from Mac to Apple Vision Pro
|
||||
- `SpatialContainer` - 3D layout container
|
||||
- Depth-based modifiers: `aspectRatio3D(_:contentMode:)`, `rotation3DLayout(_:)`, `depthAlignment(_:)`
|
||||
|
||||
### Usage Guidelines
|
||||
- Use `#available(iOS 26, *)` for iOS 26-only features
|
||||
- Replace legacy implementations with iOS 26 APIs where appropriate
|
||||
- Leverage Liquid Glass effects for modern UI aesthetics in timeline and status views
|
||||
- Use enhanced text capabilities for the status composer
|
||||
- Apply new drag-and-drop APIs for media and status interactions
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import MobileCoreServices
|
||||
import Models
|
||||
import Network
|
||||
import NetworkClient
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
|
@ -52,7 +52,7 @@ extension URL {
|
|||
guard let host = host() else {
|
||||
throw ActionRequestHandler.Error.noHost
|
||||
}
|
||||
let _: Instance = try await Client(server: host).get(endpoint: Instances.instance)
|
||||
let _: Instance = try await MastodonClient(server: host).get(endpoint: Instances.instance)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
9F7788E02BE6543D004E6BEF /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788DF2BE6543D004E6BEF /* AppAccount */; };
|
||||
9F7788E22BE6543D004E6BEF /* Env in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788E12BE6543D004E6BEF /* Env */; };
|
||||
9F7788E42BE6543D004E6BEF /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788E32BE6543D004E6BEF /* Models */; };
|
||||
9F7788E62BE6543D004E6BEF /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788E52BE6543D004E6BEF /* Network */; };
|
||||
9F7788E62BE6543D004E6BEF /* NetworkClient in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788E52BE6543D004E6BEF /* NetworkClient */; };
|
||||
9F7788F02BE78E77004E6BEF /* Timeline in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788EF2BE78E77004E6BEF /* Timeline */; };
|
||||
9F7D93942980063100EE6B7A /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7D93932980063100EE6B7A /* AppAccount */; };
|
||||
9F9191592C6DDF20001C89E7 /* WishKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9F9191582C6DDF20001C89E7 /* WishKit */; };
|
||||
|
@ -42,7 +42,7 @@
|
|||
9FAD85A2297456A400496AB1 /* Env in Frameworks */ = {isa = PBXBuildFile; productRef = 9FAD85A1297456A400496AB1 /* Env */; };
|
||||
9FAD85A4297456A800496AB1 /* DesignSystem in Frameworks */ = {isa = PBXBuildFile; productRef = 9FAD85A3297456A800496AB1 /* DesignSystem */; };
|
||||
9FAE4ACE29379A5A00772766 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 9FAE4ACD29379A5A00772766 /* KeychainSwift */; };
|
||||
9FBFE64E292A72BD00C250E9 /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = 9FBFE64D292A72BD00C250E9 /* Network */; };
|
||||
9FBFE64E292A72BD00C250E9 /* NetworkClient in Frameworks */ = {isa = PBXBuildFile; productRef = 9FBFE64D292A72BD00C250E9 /* NetworkClient */; };
|
||||
9FC2A38B2B49D19A00DFD1C1 /* StatusKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9FC2A38A2B49D19A00DFD1C1 /* StatusKit */; };
|
||||
9FC2A38D2B49D1A200DFD1C1 /* StatusKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9FC2A38C2B49D1A200DFD1C1 /* StatusKit */; };
|
||||
9FC2A38F2B49D1AA00DFD1C1 /* StatusKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9FC2A38E2B49D1AA00DFD1C1 /* StatusKit */; };
|
||||
|
@ -53,7 +53,8 @@
|
|||
9FFF6782299B7D3A00FE700A /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF6781299B7D3A00FE700A /* Account */; };
|
||||
DA0B24FB2A6876D50045BDD7 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = DA0B24FA2A6876D50045BDD7 /* SFSafeSymbols */; };
|
||||
E92817FA298443D600875FD1 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = E92817F9298443D600875FD1 /* Models */; };
|
||||
E92817FC298443D600875FD1 /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = E92817FB298443D600875FD1 /* Network */; };
|
||||
E92817FC298443D600875FD1 /* NetworkClient in Frameworks */ = {isa = PBXBuildFile; productRef = E92817FB298443D600875FD1 /* NetworkClient */; };
|
||||
E9DB78982E004E4C004EAC2E /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = E9DB78972E004E4C004EAC2E /* AppIcon.icon */; };
|
||||
E9DF41FC29830FEC0003AAD2 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E9DF41FB29830FEC0003AAD2 /* UniformTypeIdentifiers.framework */; };
|
||||
E9DF420729830FEC0003AAD2 /* IceCubesActionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E9DF41FA29830FEC0003AAD2 /* IceCubesActionExtension.appex */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
/* End PBXBuildFile section */
|
||||
|
@ -107,7 +108,7 @@
|
|||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
9F29553D292B67B600E0E81B /* Network */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Network; path = Packages/Network; sourceTree = "<group>"; };
|
||||
9F29553D292B67B600E0E81B /* NetworkClient */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = NetworkClient; path = Packages/NetworkClient; sourceTree = "<group>"; };
|
||||
9F29553E292B6AF600E0E81B /* Timeline */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Timeline; path = Packages/Timeline; sourceTree = "<group>"; };
|
||||
9F2A5404296995FB009B2D7C /* QuickLookUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLookUI.framework; path = System/Library/Frameworks/QuickLookUI.framework; sourceTree = SDKROOT; };
|
||||
9F2A540D2969A0B0009B2D7C /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
|
||||
|
@ -132,6 +133,7 @@
|
|||
9FE0346A2ADD59AC00529EA8 /* MediaUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MediaUI; path = Packages/MediaUI; sourceTree = "<group>"; };
|
||||
9FE3DB55296FEF5800628CB0 /* AppAccount */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AppAccount; path = Packages/AppAccount; sourceTree = "<group>"; };
|
||||
DD31E2E5297FB68B00A4BE29 /* IceCubesApp.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = IceCubesApp.xcconfig; sourceTree = "<group>"; };
|
||||
E9DB78972E004E4C004EAC2E /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = "<group>"; };
|
||||
E9DF41FA29830FEC0003AAD2 /* IceCubesActionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = IceCubesActionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E9DF41FB29830FEC0003AAD2 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
|
||||
/* End PBXFileReference section */
|
||||
|
@ -214,6 +216,7 @@
|
|||
9FF305672CCA8515007B6B8F /* IceCubesActionExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (9FF3056D2CCA8515007B6B8F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = IceCubesActionExtension; sourceTree = "<group>"; };
|
||||
9FF305712CCA8528007B6B8F /* IceCubesShareExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (9FF305732CCA8528007B6B8F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = IceCubesShareExtension; sourceTree = "<group>"; };
|
||||
9FF305C02CCA8569007B6B8F /* IceCubesApp */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (9FF305FB2CCA856A007B6B8F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 9FF305FC2CCA856A007B6B8F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 9FF305FD2CCA856A007B6B8F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = IceCubesApp; sourceTree = "<group>"; };
|
||||
E9FB0ACA2E09B13C002D232F /* AlternateIcons */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = AlternateIcons; sourceTree = "<group>"; };
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -237,7 +240,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9F7788E42BE6543D004E6BEF /* Models in Frameworks */,
|
||||
9F7788E62BE6543D004E6BEF /* Network in Frameworks */,
|
||||
9F7788E62BE6543D004E6BEF /* NetworkClient in Frameworks */,
|
||||
9F7788E02BE6543D004E6BEF /* AppAccount in Frameworks */,
|
||||
9F7788DE2BE6543D004E6BEF /* Account in Frameworks */,
|
||||
9F7788E22BE6543D004E6BEF /* Env in Frameworks */,
|
||||
|
@ -273,7 +276,7 @@
|
|||
9F7335EA2966B3F800AFF0BA /* Conversations in Frameworks */,
|
||||
9FE3DB57296FEFCA00628CB0 /* AppAccount in Frameworks */,
|
||||
9F398AA92935FFDB00A889F2 /* Account in Frameworks */,
|
||||
9FBFE64E292A72BD00C250E9 /* Network in Frameworks */,
|
||||
9FBFE64E292A72BD00C250E9 /* NetworkClient in Frameworks */,
|
||||
9FD542E72962D2FF0045321A /* Lists in Frameworks */,
|
||||
9F398AAB2935FFDB00A889F2 /* Models in Frameworks */,
|
||||
9F5E581929545BE700A53960 /* Env in Frameworks */,
|
||||
|
@ -290,7 +293,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E92817FC298443D600875FD1 /* Network in Frameworks */,
|
||||
E92817FC298443D600875FD1 /* NetworkClient in Frameworks */,
|
||||
E92817FA298443D600875FD1 /* Models in Frameworks */,
|
||||
E9DF41FC29830FEC0003AAD2 /* UniformTypeIdentifiers.framework in Frameworks */,
|
||||
);
|
||||
|
@ -302,6 +305,8 @@
|
|||
9FBFE630292A715500C250E9 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E9FB0ACA2E09B13C002D232F /* AlternateIcons */,
|
||||
E9DB78972E004E4C004EAC2E /* AppIcon.icon */,
|
||||
DD31E2E5297FB68B00A4BE29 /* IceCubesApp.xcconfig */,
|
||||
9F7D939529800B0300EE6B7A /* IceCubesApp-release.xcconfig */,
|
||||
9FF305C02CCA8569007B6B8F /* IceCubesApp */,
|
||||
|
@ -321,7 +326,7 @@
|
|||
9F398AA32935F90100A889F2 /* Models */,
|
||||
9FC2A3892B49D10000DFD1C1 /* StatusKit */,
|
||||
9FE0346A2ADD59AC00529EA8 /* MediaUI */,
|
||||
9F29553D292B67B600E0E81B /* Network */,
|
||||
9F29553D292B67B600E0E81B /* NetworkClient */,
|
||||
9FD542E52962D2CE0045321A /* Lists */,
|
||||
9F35DB4829506F7F00B3281A /* Notifications */,
|
||||
9F29553E292B6AF600E0E81B /* Timeline */,
|
||||
|
@ -405,7 +410,7 @@
|
|||
9F7788DF2BE6543D004E6BEF /* AppAccount */,
|
||||
9F7788E12BE6543D004E6BEF /* Env */,
|
||||
9F7788E32BE6543D004E6BEF /* Models */,
|
||||
9F7788E52BE6543D004E6BEF /* Network */,
|
||||
9F7788E52BE6543D004E6BEF /* NetworkClient */,
|
||||
9F7788EF2BE78E77004E6BEF /* Timeline */,
|
||||
);
|
||||
productName = IceCubesAppWidgetsExtensionExtension;
|
||||
|
@ -458,10 +463,11 @@
|
|||
fileSystemSynchronizedGroups = (
|
||||
9FF304F52CCA8440007B6B8F /* IceCubesAppIntents */,
|
||||
9FF305C02CCA8569007B6B8F /* IceCubesApp */,
|
||||
E9FB0ACA2E09B13C002D232F /* AlternateIcons */,
|
||||
);
|
||||
name = IceCubesApp;
|
||||
packageProductDependencies = (
|
||||
9FBFE64D292A72BD00C250E9 /* Network */,
|
||||
9FBFE64D292A72BD00C250E9 /* NetworkClient */,
|
||||
9F29553F292B6C3400E0E81B /* Timeline */,
|
||||
9F398AA82935FFDB00A889F2 /* Account */,
|
||||
9F398AAA2935FFDB00A889F2 /* Models */,
|
||||
|
@ -499,7 +505,7 @@
|
|||
name = IceCubesActionExtension;
|
||||
packageProductDependencies = (
|
||||
E92817F9298443D600875FD1 /* Models */,
|
||||
E92817FB298443D600875FD1 /* Network */,
|
||||
E92817FB298443D600875FD1 /* NetworkClient */,
|
||||
);
|
||||
productName = IceCubesActionExtension;
|
||||
productReference = E9DF41FA29830FEC0003AAD2 /* IceCubesActionExtension.appex */;
|
||||
|
@ -605,6 +611,7 @@
|
|||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E9DB78982E004E4C004EAC2E /* AppIcon.icon in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -706,13 +713,13 @@
|
|||
INFOPLIST_FILE = IceCubesNotifications/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = IceCubesNotifications;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.12.0;
|
||||
MARKETING_VERSION = 2.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -742,13 +749,13 @@
|
|||
INFOPLIST_FILE = IceCubesNotifications/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = IceCubesNotifications;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.12.0;
|
||||
MARKETING_VERSION = 2.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -781,13 +788,13 @@
|
|||
INFOPLIST_FILE = IceCubesAppWidgetsExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = IceCubesAppWidgetsExtension;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.12.0;
|
||||
MARKETING_VERSION = 2.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesAppWidgetsExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -817,13 +824,13 @@
|
|||
INFOPLIST_FILE = IceCubesAppWidgetsExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = IceCubesAppWidgetsExtension;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.12.0;
|
||||
MARKETING_VERSION = 2.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesAppWidgetsExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -850,13 +857,13 @@
|
|||
INFOPLIST_FILE = IceCubesShareExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Ice Cubes";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.12.0;
|
||||
MARKETING_VERSION = 2.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -884,13 +891,13 @@
|
|||
INFOPLIST_FILE = IceCubesShareExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Ice Cubes";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.12.0;
|
||||
MARKETING_VERSION = 2.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -943,6 +950,7 @@
|
|||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = Z6P74P6T99;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
|
@ -1010,6 +1018,7 @@
|
|||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = Z6P74P6T99;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
|
@ -1071,14 +1080,15 @@
|
|||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 1.12.0;
|
||||
MARKETING_VERSION = 2.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp";
|
||||
PRODUCT_NAME = "Ice Cubes";
|
||||
SDKROOT = auto;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
|
@ -1139,14 +1149,15 @@
|
|||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 1.12.0;
|
||||
MARKETING_VERSION = 2.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp";
|
||||
PRODUCT_NAME = "Ice Cubes";
|
||||
SDKROOT = auto;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
|
@ -1183,13 +1194,13 @@
|
|||
INFOPLIST_FILE = IceCubesActionExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Open in Ice Cube";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.12.0;
|
||||
MARKETING_VERSION = 2.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -1218,13 +1229,13 @@
|
|||
INFOPLIST_FILE = IceCubesActionExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Open in Ice Cube";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.12.0;
|
||||
MARKETING_VERSION = 2.0.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -1304,7 +1315,7 @@
|
|||
repositoryURL = "https://github.com/RevenueCat/purchases-ios-spm";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 5.21.0;
|
||||
minimumVersion = 5.28.0;
|
||||
};
|
||||
};
|
||||
9F9191572C6DDF20001C89E7 /* XCRemoteSwiftPackageReference "wishkit-ios" */ = {
|
||||
|
@ -1396,9 +1407,9 @@
|
|||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Models;
|
||||
};
|
||||
9F7788E52BE6543D004E6BEF /* Network */ = {
|
||||
9F7788E52BE6543D004E6BEF /* NetworkClient */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Network;
|
||||
productName = NetworkClient;
|
||||
};
|
||||
9F7788EF2BE78E77004E6BEF /* Timeline */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
|
@ -1443,9 +1454,9 @@
|
|||
package = 9FAE4ACC29379A5A00772766 /* XCRemoteSwiftPackageReference "keychain-swift" */;
|
||||
productName = KeychainSwift;
|
||||
};
|
||||
9FBFE64D292A72BD00C250E9 /* Network */ = {
|
||||
9FBFE64D292A72BD00C250E9 /* NetworkClient */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Network;
|
||||
productName = NetworkClient;
|
||||
};
|
||||
9FC2A38A2B49D19A00DFD1C1 /* StatusKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
|
@ -1488,9 +1499,9 @@
|
|||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Models;
|
||||
};
|
||||
E92817FB298443D600875FD1 /* Network */ = {
|
||||
E92817FB298443D600875FD1 /* NetworkClient */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Network;
|
||||
productName = NetworkClient;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"originHash" : "9d42c0a5696b2ea2df5db9f4f0bcf2ed56558636f36278a222b188cc2df705a7",
|
||||
"originHash" : "f4a38cf3b71adee5c987dcb603362633b86bbbd38c6dc4c897085f53a0ea9d44",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "bodega",
|
||||
|
@ -22,10 +22,10 @@
|
|||
{
|
||||
"identity" : "emojitext",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/divadretlaw/EmojiText",
|
||||
"location" : "https://github.com/Dimillian/EmojiText",
|
||||
"state" : {
|
||||
"revision" : "3b0417959e23307d38fd0f72ba0110daa2122e3f",
|
||||
"version" : "4.2.0"
|
||||
"branch" : "fix-ios26",
|
||||
"revision" : "0305cf9c0ecfe661bbab2b834167e261b89b3d7a"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -60,8 +60,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/RevenueCat/purchases-ios-spm",
|
||||
"state" : {
|
||||
"revision" : "adde1aa9c5fdb8cb9178ae1eed0981adcff07dd3",
|
||||
"version" : "5.21.0"
|
||||
"revision" : "2f43b88b893880848983af9b885185e20850a1df",
|
||||
"version" : "5.28.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -94,7 +94,7 @@
|
|||
{
|
||||
"identity" : "swift-markdown",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-markdown",
|
||||
"location" : "https://github.com/swiftlang/swift-markdown",
|
||||
"state" : {
|
||||
"revision" : "ea79e83c8744d2b50b0dc2d5bbd1e857e1253bf9",
|
||||
"version" : "0.6.0"
|
||||
|
|
|
@ -5,54 +5,70 @@ import DesignSystem
|
|||
import Env
|
||||
import KeychainSwift
|
||||
import MediaUI
|
||||
import Network
|
||||
import Models
|
||||
import NetworkClient
|
||||
import RevenueCat
|
||||
import StatusKit
|
||||
import SwiftData
|
||||
import SwiftUI
|
||||
import Timeline
|
||||
|
||||
@MainActor
|
||||
struct AppView: View {
|
||||
@Environment(\.modelContext) private var context
|
||||
@Environment(\.openWindow) var openWindow
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
|
||||
@Environment(AppAccountsManager.self) private var appAccountsManager
|
||||
@Environment(UserPreferences.self) private var userPreferences
|
||||
@Environment(Theme.self) private var theme
|
||||
@Environment(StreamWatcher.self) private var watcher
|
||||
|
||||
@Environment(\.openWindow) var openWindow
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
@Environment(CurrentAccount.self) private var currentAccount
|
||||
|
||||
@Binding var selectedTab: AppTab
|
||||
@Binding var appRouterPath: RouterPath
|
||||
|
||||
@State var iosTabs = iOSTabs.shared
|
||||
@State var sidebarTabs = SidebarTabs.shared
|
||||
@State var selectedTabScrollToTop: Int = -1
|
||||
@State var timeline: TimelineFilter = .home
|
||||
|
||||
@AppStorage("timeline_pinned_filters") private var pinnedFilters: [TimelineFilter] = []
|
||||
|
||||
@Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline]
|
||||
@Query(sort: \TagGroup.creationDate, order: .reverse) var tagGroups: [TagGroup]
|
||||
|
||||
var body: some View {
|
||||
switch UIDevice.current.userInterfaceIdiom {
|
||||
case .vision:
|
||||
tabBarView
|
||||
case .pad, .mac:
|
||||
#if !os(visionOS)
|
||||
sidebarView
|
||||
#else
|
||||
tabBarView
|
||||
#endif
|
||||
default:
|
||||
HStack(spacing: 0) {
|
||||
tabBarView
|
||||
.tabViewStyle(.sidebarAdaptable)
|
||||
if (horizontalSizeClass == .regular && (UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac)),
|
||||
appAccountsManager.currentClient.isAuth,
|
||||
userPreferences.showiPadSecondaryColumn
|
||||
{
|
||||
Divider().edgesIgnoringSafeArea(.all)
|
||||
notificationsSecondaryColumn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var availableTabs: [AppTab] {
|
||||
var availableSections: [SidebarSections] {
|
||||
guard appAccountsManager.currentClient.isAuth else {
|
||||
return AppTab.loggedOutTab()
|
||||
return [SidebarSections.loggedOutTabs]
|
||||
}
|
||||
if UIDevice.current.userInterfaceIdiom == .phone || horizontalSizeClass == .compact {
|
||||
return iosTabs.tabs
|
||||
return [SidebarSections.iosTabs]
|
||||
} else if UIDevice.current.userInterfaceIdiom == .vision {
|
||||
return AppTab.visionOSTab()
|
||||
return [SidebarSections.visionOSTabs]
|
||||
}
|
||||
return sidebarTabs.tabs.map { $0.tab }
|
||||
var sections = SidebarSections.macOrIpadOSSections
|
||||
if !localTimelines.isEmpty {
|
||||
sections.append(.localTimeline)
|
||||
}
|
||||
if !tagGroups.isEmpty {
|
||||
sections.append(.tagGroup)
|
||||
}
|
||||
sections.append(.app)
|
||||
return sections
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
@ -66,19 +82,49 @@ struct AppView: View {
|
|||
updateTab(with: newTab)
|
||||
})
|
||||
) {
|
||||
ForEach(availableTabs) { tab in
|
||||
tab.makeContentView(selectedTab: $selectedTab)
|
||||
.tabItem {
|
||||
if userPreferences.showiPhoneTabLabel {
|
||||
tab.label
|
||||
.environment(\.symbolVariants, tab == selectedTab ? .fill : .none)
|
||||
ForEach(availableSections) { section in
|
||||
TabSection(section.title) {
|
||||
if section == .localTimeline {
|
||||
ForEach(localTimelines) { timeline in
|
||||
let tab = AppTab.anyTimelineFilter(
|
||||
filter: .remoteLocal(server: timeline.instance, filter: .local))
|
||||
Tab(value: tab) {
|
||||
tab.makeContentView(
|
||||
homeTimeline: $timeline, selectedTab: $selectedTab, pinnedFilters: $pinnedFilters)
|
||||
} label: {
|
||||
tab.label.environment(\.symbolVariants, tab == selectedTab ? .fill : .none)
|
||||
}
|
||||
.tabPlacement(tab.tabPlacement)
|
||||
}
|
||||
} else if section == .tagGroup {
|
||||
ForEach(tagGroups) { tagGroup in
|
||||
let tab = AppTab.anyTimelineFilter(
|
||||
filter: TimelineFilter.tagGroup(
|
||||
title: tagGroup.title,
|
||||
tags: tagGroup.tags,
|
||||
symbolName: tagGroup.symbolName))
|
||||
Tab(value: tab) {
|
||||
tab.makeContentView(
|
||||
homeTimeline: $timeline, selectedTab: $selectedTab, pinnedFilters: $pinnedFilters)
|
||||
} label: {
|
||||
tab.label.environment(\.symbolVariants, tab == selectedTab ? .fill : .none)
|
||||
}
|
||||
.tabPlacement(tab.tabPlacement)
|
||||
}
|
||||
} else {
|
||||
Image(systemName: tab.iconName)
|
||||
ForEach(section.tabs) { tab in
|
||||
Tab(value: tab, role: tab == .explore ? .search : .none) {
|
||||
tab.makeContentView(
|
||||
homeTimeline: $timeline, selectedTab: $selectedTab, pinnedFilters: $pinnedFilters)
|
||||
} label: {
|
||||
tab.label.environment(\.symbolVariants, tab == selectedTab ? .fill : .none)
|
||||
}
|
||||
}
|
||||
.tag(tab)
|
||||
.tabPlacement(tab.tabPlacement)
|
||||
.badge(badgeFor(tab: tab))
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .tabBar)
|
||||
}
|
||||
}
|
||||
}
|
||||
.tabPlacement(.sidebarOnly)
|
||||
}
|
||||
}
|
||||
.id(appAccountsManager.currentClient.id)
|
||||
|
@ -102,7 +148,7 @@ struct AppView: View {
|
|||
SoundEffectManager.shared.playSound(.tabSelection)
|
||||
|
||||
if selectedTab == newTab {
|
||||
selectedTabScrollToTop = newTab.rawValue
|
||||
selectedTabScrollToTop = newTab.id
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
selectedTabScrollToTop = -1
|
||||
}
|
||||
|
@ -122,66 +168,6 @@ struct AppView: View {
|
|||
return 0
|
||||
}
|
||||
|
||||
#if !os(visionOS)
|
||||
var sidebarView: some View {
|
||||
SideBarView(
|
||||
selectedTab: .init(
|
||||
get: {
|
||||
selectedTab
|
||||
},
|
||||
set: { newTab in
|
||||
updateTab(with: newTab)
|
||||
}), tabs: availableTabs
|
||||
) {
|
||||
HStack(spacing: 0) {
|
||||
if #available(iOS 18.0, *) {
|
||||
baseTabView
|
||||
#if targetEnvironment(macCatalyst)
|
||||
.tabViewStyle(.sidebarAdaptable)
|
||||
.introspect(.tabView, on: .iOS(.v17, .v18)) { (tabview: UITabBarController) in
|
||||
tabview.sidebar.isHidden = true
|
||||
}
|
||||
#else
|
||||
.tabViewStyle(.tabBarOnly)
|
||||
#endif
|
||||
} else {
|
||||
baseTabView
|
||||
}
|
||||
if horizontalSizeClass == .regular,
|
||||
appAccountsManager.currentClient.isAuth,
|
||||
userPreferences.showiPadSecondaryColumn
|
||||
{
|
||||
Divider().edgesIgnoringSafeArea(.all)
|
||||
notificationsSecondaryColumn
|
||||
}
|
||||
}
|
||||
}
|
||||
.environment(appRouterPath)
|
||||
.environment(\.selectedTabScrollToTop, selectedTabScrollToTop)
|
||||
}
|
||||
#endif
|
||||
|
||||
private var baseTabView: some View {
|
||||
TabView(selection: $selectedTab) {
|
||||
ForEach(availableTabs) { tab in
|
||||
tab
|
||||
.makeContentView(selectedTab: $selectedTab)
|
||||
.toolbar(horizontalSizeClass == .regular ? .hidden : .visible, for: .tabBar)
|
||||
.tabItem {
|
||||
tab.label
|
||||
}
|
||||
.tag(tab)
|
||||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.introspect(.tabView, on: .iOS(.v17, .v18)) { (tabview: UITabBarController) in
|
||||
tabview.tabBar.isHidden = horizontalSizeClass == .regular
|
||||
tabview.customizableViewControllers = []
|
||||
tabview.moreNavigationController.isNavigationBarHidden = true
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
var notificationsSecondaryColumn: some View {
|
||||
NotificationsTab(selectedTab: .constant(.notifications), lockedType: nil)
|
||||
.environment(\.isSecondaryColumn, true)
|
||||
|
|
|
@ -26,23 +26,18 @@ extension IceCubesApp {
|
|||
.environment(appIntentService)
|
||||
.environment(\.isSupporter, isSupporter)
|
||||
.sheet(item: $quickLook.selectedMediaAttachment) { selectedMediaAttachment in
|
||||
if #available(iOS 18.0, *) {
|
||||
if let namespace = quickLook.namespace {
|
||||
MediaUIView(
|
||||
selectedAttachment: selectedMediaAttachment,
|
||||
attachments: quickLook.mediaAttachments
|
||||
)
|
||||
.presentationBackground(.ultraThinMaterial)
|
||||
.navigationTransition(.zoom(sourceID: selectedMediaAttachment.id, in: namespace))
|
||||
.presentationBackground(theme.primaryBackgroundColor)
|
||||
.presentationCornerRadius(16)
|
||||
.presentationSizing(.page)
|
||||
.withEnvironments()
|
||||
} else {
|
||||
MediaUIView(
|
||||
selectedAttachment: selectedMediaAttachment,
|
||||
attachments: quickLook.mediaAttachments
|
||||
)
|
||||
.presentationBackground(.ultraThinMaterial)
|
||||
.presentationCornerRadius(16)
|
||||
.withEnvironments()
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.onChange(of: pushNotificationsService.handledNotification) { _, newValue in
|
||||
|
@ -164,6 +159,8 @@ extension IceCubesApp {
|
|||
{
|
||||
appRouterPath.presentedSheet = .imageURL(
|
||||
urls: urls,
|
||||
caption: imageIntent.caption,
|
||||
altTexts: imageIntent.altText.map { [$0] },
|
||||
visibility: userPreferences.postVisibility)
|
||||
}
|
||||
}
|
||||
|
@ -171,10 +168,6 @@ extension IceCubesApp {
|
|||
|
||||
extension Scene {
|
||||
func windowResize() -> some Scene {
|
||||
if #available(iOS 18.0, *) {
|
||||
return self.windowResizability(.contentSize)
|
||||
} else {
|
||||
return self.defaultSize(width: 1100, height: 1400)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import DesignSystem
|
|||
import Env
|
||||
import KeychainSwift
|
||||
import MediaUI
|
||||
import Network
|
||||
import NetworkClient
|
||||
import RevenueCat
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
|
@ -34,6 +34,8 @@ struct IceCubesApp: App {
|
|||
|
||||
@State var isSupporter: Bool = false
|
||||
|
||||
@Namespace var namespace
|
||||
|
||||
init() {
|
||||
#if DEBUG
|
||||
// Enable "GraphReuseLogging" for debugging purpose
|
||||
|
@ -47,7 +49,8 @@ struct IceCubesApp: App {
|
|||
otherScenes
|
||||
}
|
||||
|
||||
func setNewClientsInEnv(client: Client) {
|
||||
func setNewClientsInEnv(client: MastodonClient) {
|
||||
quickLook.namespace = namespace
|
||||
currentAccount.setClient(client: client)
|
||||
currentInstance.setClient(client: client)
|
||||
userPreferences.setClient(client: client)
|
||||
|
@ -141,6 +144,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
super.buildMenu(with: builder)
|
||||
builder.remove(menu: .document)
|
||||
builder.remove(menu: .toolbar)
|
||||
builder.remove(menu: .sidebar)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,261 +0,0 @@
|
|||
import Account
|
||||
import AppAccount
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import SwiftUI
|
||||
import SwiftUIIntrospect
|
||||
|
||||
@MainActor
|
||||
struct SideBarView<Content: View>: View {
|
||||
@Environment(\.openWindow) private var openWindow
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
|
||||
@Environment(AppAccountsManager.self) private var appAccounts
|
||||
@Environment(CurrentAccount.self) private var currentAccount
|
||||
@Environment(Theme.self) private var theme
|
||||
@Environment(StreamWatcher.self) private var watcher
|
||||
@Environment(UserPreferences.self) private var userPreferences
|
||||
@Environment(RouterPath.self) private var routerPath
|
||||
|
||||
@Binding var selectedTab: AppTab
|
||||
var tabs: [AppTab]
|
||||
@ViewBuilder var content: () -> Content
|
||||
|
||||
@State private var sidebarTabs = SidebarTabs.shared
|
||||
|
||||
private func badgeFor(tab: AppTab) -> Int {
|
||||
if tab == .notifications, selectedTab != tab,
|
||||
let token = appAccounts.currentAccount.oauthToken
|
||||
{
|
||||
return watcher.unreadNotificationsCount + (userPreferences.notificationsCount[token] ?? 0)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
private func makeIconForTab(tab: AppTab) -> some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
HStack {
|
||||
SideBarIcon(
|
||||
systemIconName: tab.iconName,
|
||||
isSelected: tab == selectedTab)
|
||||
if userPreferences.isSidebarExpanded {
|
||||
Text(tab.title)
|
||||
.font(.headline)
|
||||
.foregroundColor(tab == selectedTab ? theme.tintColor : theme.labelColor)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
.frame(
|
||||
width: (userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth) - 24,
|
||||
height: 50
|
||||
)
|
||||
.background(
|
||||
tab == selectedTab ? theme.primaryBackgroundColor : .clear,
|
||||
in: RoundedRectangle(cornerRadius: 8)
|
||||
)
|
||||
.cornerRadius(8)
|
||||
.shadow(color: tab == selectedTab ? .black.opacity(0.2) : .clear, radius: 5)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(tab == selectedTab ? theme.labelColor.opacity(0.1) : .clear, lineWidth: 1)
|
||||
)
|
||||
let badge = badgeFor(tab: tab)
|
||||
if badge > 0 {
|
||||
makeBadgeView(count: badge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func makeBadgeView(count: Int) -> some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(.red)
|
||||
Text(count > 99 ? "99+" : String(count))
|
||||
.foregroundColor(.white)
|
||||
.font(.caption2)
|
||||
}
|
||||
.frame(width: 24, height: 24)
|
||||
.offset(x: 5, y: -5)
|
||||
}
|
||||
|
||||
private var postButton: some View {
|
||||
Button {
|
||||
#if targetEnvironment(macCatalyst) || os(visionOS)
|
||||
openWindow(
|
||||
value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility)
|
||||
)
|
||||
#else
|
||||
routerPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
|
||||
#endif
|
||||
} label: {
|
||||
Image(systemName: "square.and.pencil")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 20, height: 30)
|
||||
.offset(x: 2, y: -2)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.help(AppTab.post.title)
|
||||
}
|
||||
|
||||
private func makeAccountButton(account: AppAccount, showBadge: Bool) -> some View {
|
||||
Button {
|
||||
if account.id == appAccounts.currentAccount.id {
|
||||
selectedTab = .profile
|
||||
SoundEffectManager.shared.playSound(.tabSelection)
|
||||
} else {
|
||||
var transation = Transaction()
|
||||
transation.disablesAnimations = true
|
||||
withTransaction(transation) {
|
||||
appAccounts.currentAccount = account
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
if userPreferences.isSidebarExpanded {
|
||||
AppAccountView(
|
||||
viewModel: .init(
|
||||
appAccount: account,
|
||||
isCompact: false,
|
||||
isInSettings: false),
|
||||
isParentPresented: .constant(false))
|
||||
} else {
|
||||
AppAccountView(
|
||||
viewModel: .init(
|
||||
appAccount: account,
|
||||
isCompact: true,
|
||||
isInSettings: false),
|
||||
isParentPresented: .constant(false))
|
||||
}
|
||||
if !userPreferences.isSidebarExpanded,
|
||||
showBadge,
|
||||
let token = account.oauthToken,
|
||||
let notificationsCount = userPreferences.notificationsCount[token],
|
||||
notificationsCount > 0
|
||||
{
|
||||
makeBadgeView(count: notificationsCount)
|
||||
}
|
||||
}
|
||||
.padding(.leading, userPreferences.isSidebarExpanded ? 16 : 0)
|
||||
}
|
||||
.help(accountButtonTitle(accountName: account.accountName))
|
||||
.frame(
|
||||
width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth, height: 50
|
||||
)
|
||||
.padding(.vertical, 8)
|
||||
.background(
|
||||
selectedTab == .profile && account.id == appAccounts.currentAccount.id
|
||||
? theme.secondaryBackgroundColor : .clear)
|
||||
}
|
||||
|
||||
private func accountButtonTitle(accountName: String?) -> LocalizedStringKey {
|
||||
if let accountName {
|
||||
"tab.profile-account-\(accountName)"
|
||||
} else {
|
||||
AppTab.profile.title
|
||||
}
|
||||
}
|
||||
|
||||
private var tabsView: some View {
|
||||
ForEach(tabs) { tab in
|
||||
if tab != .profile && sidebarTabs.isEnabled(tab) {
|
||||
Button {
|
||||
// ensure keyboard is always dismissed when selecting a tab
|
||||
hideKeyboard()
|
||||
selectedTab = tab
|
||||
SoundEffectManager.shared.playSound(.tabSelection)
|
||||
if tab == .notifications {
|
||||
if let token = appAccounts.currentAccount.oauthToken {
|
||||
userPreferences.notificationsCount[token] = 0
|
||||
}
|
||||
watcher.unreadNotificationsCount = 0
|
||||
}
|
||||
} label: {
|
||||
makeIconForTab(tab: tab)
|
||||
}
|
||||
.help(tab.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@Bindable var routerPath = routerPath
|
||||
HStack(spacing: 0) {
|
||||
if horizontalSizeClass == .regular {
|
||||
ScrollView {
|
||||
VStack(alignment: .center) {
|
||||
if appAccounts.availableAccounts.isEmpty {
|
||||
tabsView
|
||||
} else {
|
||||
ForEach(appAccounts.availableAccounts) { account in
|
||||
makeAccountButton(
|
||||
account: account,
|
||||
showBadge: account.id != appAccounts.currentAccount.id)
|
||||
if account.id == appAccounts.currentAccount.id {
|
||||
tabsView
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(.thinMaterial)
|
||||
.safeAreaInset(
|
||||
edge: .bottom,
|
||||
content: {
|
||||
HStack(spacing: 16) {
|
||||
postButton
|
||||
.padding(.vertical, 24)
|
||||
.padding(.leading, userPreferences.isSidebarExpanded ? 18 : 0)
|
||||
if userPreferences.isSidebarExpanded {
|
||||
Text("menu.new-post")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(theme.labelColor)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
.frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth)
|
||||
.background(.thinMaterial)
|
||||
})
|
||||
Divider().edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
content()
|
||||
}
|
||||
.background(.thinMaterial)
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SideBarIcon: View {
|
||||
@Environment(Theme.self) private var theme
|
||||
|
||||
let systemIconName: String
|
||||
let isSelected: Bool
|
||||
|
||||
@State private var isHovered: Bool = false
|
||||
|
||||
var body: some View {
|
||||
Image(systemName: systemIconName)
|
||||
.font(.title2)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(isSelected ? theme.tintColor : theme.labelColor)
|
||||
.symbolVariant(isSelected ? .fill : .none)
|
||||
.scaleEffect(isHovered ? 0.8 : 1.0)
|
||||
.onHover { isHovered in
|
||||
withAnimation(.interpolatingSpring(stiffness: 300, damping: 15)) {
|
||||
self.isHovered = isHovered
|
||||
}
|
||||
}
|
||||
.frame(width: 50, height: 40)
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
@MainActor func hideKeyboard() {
|
||||
let resign = #selector(UIResponder.resignFirstResponder)
|
||||
UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil)
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import NetworkClient
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
|
||||
|
@ -9,7 +9,7 @@ public struct ReportView: View {
|
|||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@Environment(Theme.self) private var theme
|
||||
@Environment(Client.self) private var client
|
||||
@Environment(MastodonClient.self) private var client
|
||||
|
||||
let status: Status
|
||||
@State private var commentText: String = ""
|
||||
|
|
|
@ -18,65 +18,72 @@ extension View {
|
|||
func withAppRouter() -> some View {
|
||||
navigationDestination(for: RouterDestination.self) { destination in
|
||||
switch destination {
|
||||
case let .accountDetail(id):
|
||||
case .accountDetail(let id):
|
||||
AccountDetailView(accountId: id)
|
||||
case let .accountDetailWithAccount(account):
|
||||
case .accountDetailWithAccount(let account):
|
||||
AccountDetailView(account: account)
|
||||
case let .accountSettingsWithAccount(account, appAccount):
|
||||
case .accountSettingsWithAccount(let account, let appAccount):
|
||||
AccountSettingsView(account: account, appAccount: appAccount)
|
||||
case let .accountMediaGridView(account, initialMedia):
|
||||
case .accountMediaGridView(let account, let initialMedia):
|
||||
AccountDetailMediaGridView(account: account, initialMediaStatuses: initialMedia)
|
||||
case let .statusDetail(id):
|
||||
case .statusDetail(let id):
|
||||
StatusDetailView(statusId: id)
|
||||
case let .statusDetailWithStatus(status):
|
||||
case .statusDetailWithStatus(let status):
|
||||
StatusDetailView(status: status)
|
||||
case let .remoteStatusDetail(url):
|
||||
case .remoteStatusDetail(let url):
|
||||
StatusDetailView(remoteStatusURL: url)
|
||||
case let .conversationDetail(conversation):
|
||||
case .conversationDetail(let conversation):
|
||||
ConversationDetailView(conversation: conversation)
|
||||
case let .hashTag(tag, accountId):
|
||||
TimelineView(timeline: .constant(.hashtag(tag: tag, accountId: accountId)),
|
||||
case .hashTag(let tag, let accountId):
|
||||
TimelineView(
|
||||
timeline: .constant(.hashtag(tag: tag, accountId: accountId)),
|
||||
pinnedFilters: .constant([]),
|
||||
selectedTagGroup: .constant(nil),
|
||||
canFilterTimeline: false)
|
||||
case let .list(list):
|
||||
TimelineView(timeline: .constant(.list(list: list)),
|
||||
case .list(let list):
|
||||
TimelineView(
|
||||
timeline: .constant(.list(list: list)),
|
||||
pinnedFilters: .constant([]),
|
||||
selectedTagGroup: .constant(nil),
|
||||
canFilterTimeline: false)
|
||||
case let .linkTimeline(url, title):
|
||||
TimelineView(timeline: .constant(.link(url: url, title: title)),
|
||||
case .linkTimeline(let url, let title):
|
||||
TimelineView(
|
||||
timeline: .constant(.link(url: url, title: title)),
|
||||
pinnedFilters: .constant([]),
|
||||
selectedTagGroup: .constant(nil),
|
||||
canFilterTimeline: false)
|
||||
case let .following(id):
|
||||
case .following(let id):
|
||||
AccountsListView(mode: .following(accountId: id))
|
||||
case let .followers(id):
|
||||
case .followers(let id):
|
||||
AccountsListView(mode: .followers(accountId: id))
|
||||
case let .favoritedBy(id):
|
||||
case .favoritedBy(let id):
|
||||
AccountsListView(mode: .favoritedBy(statusId: id))
|
||||
case let .rebloggedBy(id):
|
||||
case .rebloggedBy(let id):
|
||||
AccountsListView(mode: .rebloggedBy(statusId: id))
|
||||
case let .accountsList(accounts):
|
||||
case .accountsList(let accounts):
|
||||
AccountsListView(mode: .accountsList(accounts: accounts))
|
||||
case .trendingTimeline:
|
||||
TimelineView(timeline: .constant(.trending),
|
||||
TimelineView(
|
||||
timeline: .constant(.trending),
|
||||
pinnedFilters: .constant([]),
|
||||
selectedTagGroup: .constant(nil),
|
||||
canFilterTimeline: false)
|
||||
case let .trendingLinks(cards):
|
||||
case .trendingLinks(let cards):
|
||||
TrendingLinksListView(cards: cards)
|
||||
case let .tagsList(tags):
|
||||
case .tagsList(let tags):
|
||||
TagsListView(tags: tags)
|
||||
case .notificationsRequests:
|
||||
NotificationsRequestsListView()
|
||||
case let .notificationForAccount(accountId):
|
||||
NotificationsListView(lockedType: nil,
|
||||
case .notificationForAccount(let accountId):
|
||||
NotificationsListView(
|
||||
lockedType: nil,
|
||||
lockedAccountId: accountId)
|
||||
case .blockedAccounts:
|
||||
AccountsListView(mode: .blocked)
|
||||
case .mutedAccounts:
|
||||
AccountsListView(mode: .muted)
|
||||
case .conversations:
|
||||
ConversationsListView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -84,37 +91,37 @@ extension View {
|
|||
func withSheetDestinations(sheetDestinations: Binding<SheetDestination?>) -> some View {
|
||||
sheet(item: sheetDestinations) { destination in
|
||||
switch destination {
|
||||
case let .replyToStatusEditor(status):
|
||||
case .replyToStatusEditor(let status):
|
||||
StatusEditor.MainView(mode: .replyTo(status: status))
|
||||
.withEnvironments()
|
||||
case let .newStatusEditor(visibility):
|
||||
case .newStatusEditor(let visibility):
|
||||
StatusEditor.MainView(mode: .new(text: nil, visibility: visibility))
|
||||
.withEnvironments()
|
||||
case let .prefilledStatusEditor(text, visibility):
|
||||
case .prefilledStatusEditor(let text, let visibility):
|
||||
StatusEditor.MainView(mode: .new(text: text, visibility: visibility))
|
||||
.withEnvironments()
|
||||
case let .imageURL(urls, visibility):
|
||||
StatusEditor.MainView(mode: .imageURL(urls: urls, visibility: visibility))
|
||||
case .imageURL(let urls, let caption, let altTexts, let visibility):
|
||||
StatusEditor.MainView(mode: .imageURL(urls: urls, caption: caption, altTexts: altTexts, visibility: visibility))
|
||||
.withEnvironments()
|
||||
case let .editStatusEditor(status):
|
||||
case .editStatusEditor(let status):
|
||||
StatusEditor.MainView(mode: .edit(status: status))
|
||||
.withEnvironments()
|
||||
case let .quoteStatusEditor(status):
|
||||
case .quoteStatusEditor(let status):
|
||||
StatusEditor.MainView(mode: .quote(status: status))
|
||||
.withEnvironments()
|
||||
case let .quoteLinkStatusEditor(link):
|
||||
case .quoteLinkStatusEditor(let link):
|
||||
StatusEditor.MainView(mode: .quoteLink(link: link))
|
||||
.withEnvironments()
|
||||
case let .mentionStatusEditor(account, visibility):
|
||||
case .mentionStatusEditor(let account, let visibility):
|
||||
StatusEditor.MainView(mode: .mention(account: account, visibility: visibility))
|
||||
.withEnvironments()
|
||||
case .listCreate:
|
||||
ListCreateView()
|
||||
.withEnvironments()
|
||||
case let .listEdit(list):
|
||||
case .listEdit(let list):
|
||||
ListEditView(list: list)
|
||||
.withEnvironments()
|
||||
case let .listAddAccount(account):
|
||||
case .listAddAccount(let account):
|
||||
ListAddAccountView(account: account)
|
||||
.withEnvironments()
|
||||
case .addAccount:
|
||||
|
@ -126,7 +133,7 @@ extension View {
|
|||
case .addTagGroup:
|
||||
EditTagGroupView()
|
||||
.withEnvironments()
|
||||
case let .statusEditHistory(status):
|
||||
case .statusEditHistory(let status):
|
||||
StatusEditHistoryView(statusId: status)
|
||||
.withEnvironments()
|
||||
case .settings:
|
||||
|
@ -134,7 +141,9 @@ extension View {
|
|||
.withEnvironments()
|
||||
.preferredColorScheme(Theme.shared.selectedScheme == .dark ? .dark : .light)
|
||||
case .accountPushNotficationsSettings:
|
||||
if let subscription = PushNotificationsService.shared.subscriptions.first(where: { $0.account.token == AppAccountsManager.shared.currentAccount.oauthToken }) {
|
||||
if let subscription = PushNotificationsService.shared.subscriptions.first(where: {
|
||||
$0.account.token == AppAccountsManager.shared.currentAccount.oauthToken
|
||||
}) {
|
||||
NavigationSheet { PushNotificationsView(subscription: subscription) }
|
||||
.withEnvironments()
|
||||
} else {
|
||||
|
@ -146,19 +155,17 @@ extension View {
|
|||
case .support:
|
||||
NavigationSheet { SupportAppView() }
|
||||
.withEnvironments()
|
||||
case let .report(status):
|
||||
case .report(let status):
|
||||
ReportView(status: status)
|
||||
.withEnvironments()
|
||||
case let .shareImage(image, status):
|
||||
case .shareImage(image: let image, status: let status):
|
||||
ActivityView(image: image, status: status)
|
||||
.withEnvironments()
|
||||
case let .editTagGroup(tagGroup, onSaved):
|
||||
case .editTagGroup(let tagGroup, let onSaved):
|
||||
EditTagGroupView(tagGroup: tagGroup, onSaved: onSaved)
|
||||
.withEnvironments()
|
||||
case .timelineContentFilter:
|
||||
NavigationSheet { TimelineContentFilterView() }
|
||||
.presentationDetents([.medium])
|
||||
.presentationBackground(.thinMaterial)
|
||||
TimelineContentFilterView()
|
||||
.withEnvironments()
|
||||
case .accountEditInfo:
|
||||
EditAccountView()
|
||||
|
@ -216,19 +223,25 @@ struct ActivityView: UIViewControllerRepresentable {
|
|||
image
|
||||
}
|
||||
|
||||
func activityViewController(_: UIActivityViewController,
|
||||
itemForActivityType _: UIActivity.ActivityType?) -> Any?
|
||||
{
|
||||
func activityViewController(
|
||||
_: UIActivityViewController,
|
||||
itemForActivityType _: UIActivity.ActivityType?
|
||||
) -> Any? {
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
func makeUIViewController(context _: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
|
||||
UIActivityViewController(activityItems: [image, LinkDelegate(image: image, status: status)],
|
||||
func makeUIViewController(context _: UIViewControllerRepresentableContext<ActivityView>)
|
||||
-> UIActivityViewController
|
||||
{
|
||||
UIActivityViewController(
|
||||
activityItems: [image, LinkDelegate(image: image, status: status)],
|
||||
applicationActivities: nil)
|
||||
}
|
||||
|
||||
func updateUIViewController(_: UIActivityViewController, context _: UIViewControllerRepresentableContext<ActivityView>) {}
|
||||
func updateUIViewController(
|
||||
_: UIActivityViewController, context _: UIViewControllerRepresentableContext<ActivityView>
|
||||
) {}
|
||||
}
|
||||
|
||||
extension URL: @retroactive Identifiable {
|
||||
|
|
|
@ -60,20 +60,6 @@ private struct SafariRouter: ViewModifier {
|
|||
UIApplication.shared.open(url)
|
||||
return .handled
|
||||
}
|
||||
} else if url.query()?.contains("callback=") == false,
|
||||
url.host() == AppInfo.premiumInstance,
|
||||
let accountName = appAccount.currentAccount.accountName
|
||||
{
|
||||
let newURL = url.appending(queryItems: [
|
||||
.init(name: "callback", value: "icecubesapp://subclub"),
|
||||
.init(name: "id", value: "@\(accountName)"),
|
||||
])
|
||||
|
||||
#if !os(visionOS)
|
||||
return safariManager.open(newURL)
|
||||
#else
|
||||
return .systemAction
|
||||
#endif
|
||||
}
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
guard preferences.preferredBrowser == .inAppSafari else { return .systemAction }
|
||||
|
|
|
@ -3,7 +3,7 @@ import DesignSystem
|
|||
import Env
|
||||
import Explore
|
||||
import Models
|
||||
import Network
|
||||
import NetworkClient
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
|
@ -11,7 +11,7 @@ struct ExploreTab: View {
|
|||
@Environment(Theme.self) private var theme
|
||||
@Environment(UserPreferences.self) private var preferences
|
||||
@Environment(CurrentAccount.self) private var currentAccount
|
||||
@Environment(Client.self) private var client
|
||||
@Environment(MastodonClient.self) private var client
|
||||
@State private var routerPath = RouterPath()
|
||||
|
||||
var body: some View {
|
||||
|
@ -19,7 +19,6 @@ struct ExploreTab: View {
|
|||
ExploreView()
|
||||
.withAppRouter()
|
||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||
.toolbar {
|
||||
ToolbarTab(routerPath: $routerPath)
|
||||
}
|
||||
|
|
|
@ -4,14 +4,14 @@ import Conversations
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import NetworkClient
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
struct MessagesTab: View {
|
||||
@Environment(Theme.self) private var theme
|
||||
@Environment(StreamWatcher.self) private var watcher
|
||||
@Environment(Client.self) private var client
|
||||
@Environment(MastodonClient.self) private var client
|
||||
@Environment(CurrentAccount.self) private var currentAccount
|
||||
@Environment(AppAccountsManager.self) private var appAccount
|
||||
@State private var routerPath = RouterPath()
|
||||
|
@ -24,7 +24,6 @@ struct MessagesTab: View {
|
|||
.toolbar {
|
||||
ToolbarTab(routerPath: $routerPath)
|
||||
}
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||
.id(client.id)
|
||||
}
|
||||
.onChange(of: client.id) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import AppAccount
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Network
|
||||
import NetworkClient
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
|
@ -12,7 +12,7 @@ struct NavigationTab<Content: View>: View {
|
|||
@Environment(CurrentAccount.self) private var currentAccount
|
||||
@Environment(UserPreferences.self) private var userPreferences
|
||||
@Environment(Theme.self) private var theme
|
||||
@Environment(Client.self) private var client
|
||||
@Environment(MastodonClient.self) private var client
|
||||
|
||||
var content: () -> Content
|
||||
|
||||
|
@ -32,7 +32,6 @@ struct NavigationTab<Content: View>: View {
|
|||
.toolbar {
|
||||
ToolbarTab(routerPath: $routerPath)
|
||||
}
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||
.onChange(of: client.id) {
|
||||
routerPath.path = []
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import AppAccount
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import NetworkClient
|
||||
import Notifications
|
||||
import SwiftUI
|
||||
import Timeline
|
||||
|
@ -13,7 +13,7 @@ struct NotificationsTab: View {
|
|||
@Environment(\.scenePhase) private var scenePhase
|
||||
|
||||
@Environment(Theme.self) private var theme
|
||||
@Environment(Client.self) private var client
|
||||
@Environment(MastodonClient.self) private var client
|
||||
@Environment(StreamWatcher.self) private var watcher
|
||||
@Environment(AppAccountsManager.self) private var appAccount
|
||||
@Environment(CurrentAccount.self) private var currentAccount
|
||||
|
@ -37,16 +37,21 @@ struct NotificationsTab: View {
|
|||
} label: {
|
||||
Image(systemName: "bell")
|
||||
}
|
||||
.tint(.label)
|
||||
}
|
||||
if #available(iOS 26.0, *) {
|
||||
ToolbarSpacer(placement: .topBarTrailing)
|
||||
}
|
||||
ToolbarTab(routerPath: $routerPath)
|
||||
}
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||
.id(client.id)
|
||||
}
|
||||
.onAppear {
|
||||
routerPath.client = client
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||
clearNotifications()
|
||||
}
|
||||
}
|
||||
.withSafariRouter()
|
||||
.environment(routerPath)
|
||||
.onChange(of: selectedTab) { _, _ in
|
||||
|
|
|
@ -4,14 +4,14 @@ import Conversations
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import NetworkClient
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
struct ProfileTab: View {
|
||||
@Environment(AppAccountsManager.self) private var appAccount
|
||||
@Environment(Theme.self) private var theme
|
||||
@Environment(Client.self) private var client
|
||||
@Environment(MastodonClient.self) private var client
|
||||
@Environment(CurrentAccount.self) private var currentAccount
|
||||
@State private var routerPath = RouterPath()
|
||||
|
||||
|
@ -21,7 +21,6 @@ struct ProfileTab: View {
|
|||
AccountDetailView(account: account)
|
||||
.withAppRouter()
|
||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||
.id(account.id)
|
||||
} else {
|
||||
AccountDetailView(account: .placeholder())
|
||||
|
|
|
@ -2,14 +2,14 @@ import Account
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import NetworkClient
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
struct AboutView: View {
|
||||
@Environment(RouterPath.self) private var routerPath
|
||||
@Environment(Theme.self) private var theme
|
||||
@Environment(Client.self) private var client
|
||||
@Environment(MastodonClient.self) private var client
|
||||
|
||||
@State private var dimillianAccount: AccountsListRowViewModel?
|
||||
@State private var iceCubesAccount: AccountsListRowViewModel?
|
||||
|
@ -176,7 +176,7 @@ struct AboutView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func fetchAccountViewModel(_ client: Client, account: String) async throws
|
||||
private func fetchAccountViewModel(_ client: MastodonClient, account: String) async throws
|
||||
-> AccountsListRowViewModel
|
||||
{
|
||||
let dimillianAccount: Account = try await client.get(endpoint: Accounts.lookup(name: account))
|
||||
|
|
|
@ -3,7 +3,7 @@ import AppAccount
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import NetworkClient
|
||||
import SwiftUI
|
||||
import Timeline
|
||||
|
||||
|
@ -16,7 +16,7 @@ struct AccountSettingsView: View {
|
|||
@Environment(CurrentInstance.self) private var currentInstance
|
||||
@Environment(Theme.self) private var theme
|
||||
@Environment(AppAccountsManager.self) private var appAccountsManager
|
||||
@Environment(Client.self) private var client
|
||||
@Environment(MastodonClient.self) private var client
|
||||
@Environment(RouterPath.self) private var routerPath
|
||||
|
||||
@State private var cachedPostsCount: Int = 0
|
||||
|
@ -84,7 +84,7 @@ struct AccountSettingsView: View {
|
|||
Button(role: .destructive) {
|
||||
if let token = appAccount.oauthToken {
|
||||
Task {
|
||||
let client = Client(server: appAccount.server, oauthToken: token)
|
||||
let client = MastodonClient(server: appAccount.server, oauthToken: token)
|
||||
await timelineCache.clearCache(for: client.id)
|
||||
if let sub = pushNotifications.subscriptions.first(where: {
|
||||
$0.account.token == token
|
||||
|
|
|
@ -4,7 +4,7 @@ import Combine
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import NetworkClient
|
||||
import NukeUI
|
||||
import SafariServices
|
||||
import SwiftUI
|
||||
|
@ -25,7 +25,7 @@ struct AddAccountView: View {
|
|||
@State private var instanceName: String = ""
|
||||
@State private var instance: Instance?
|
||||
@State private var isSigninIn = false
|
||||
@State private var signInClient: Client?
|
||||
@State private var signInClient: MastodonClient?
|
||||
@State private var instances: [InstanceSocial] = []
|
||||
@State private var instanceFetchError: LocalizedStringKey?
|
||||
@State private var instanceSocialClient = InstanceSocialClient()
|
||||
|
@ -123,7 +123,7 @@ struct AddAccountView: View {
|
|||
|
||||
do {
|
||||
// bare bones preflight for domain validity
|
||||
let instanceDetailClient = Client(server: sanitizedName)
|
||||
let instanceDetailClient = MastodonClient(server: sanitizedName)
|
||||
if instanceDetailClient.server.contains("."),
|
||||
instanceDetailClient.server.last != "."
|
||||
{
|
||||
|
@ -160,6 +160,17 @@ struct AddAccountView: View {
|
|||
|
||||
private var signInSection: some View {
|
||||
Section {
|
||||
if #available(iOS 26.0, *) {
|
||||
signinButton
|
||||
.buttonStyle(.glassProminent)
|
||||
} else {
|
||||
signinButton
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var signinButton: some View {
|
||||
Button {
|
||||
withAnimation {
|
||||
isSigninIn = true
|
||||
|
@ -169,7 +180,6 @@ struct AddAccountView: View {
|
|||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
if isSigninIn || !sanitizedName.isEmpty && instance == nil {
|
||||
ProgressView()
|
||||
.id(sanitizedName)
|
||||
|
@ -178,14 +188,12 @@ struct AddAccountView: View {
|
|||
Text("account.add.sign-in")
|
||||
.font(.scaledHeadline)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 44)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.tintColor)
|
||||
#endif
|
||||
.listRowInsets(.init())
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
|
||||
private var instancesListView: some View {
|
||||
|
@ -294,7 +302,7 @@ struct AddAccountView: View {
|
|||
}
|
||||
do {
|
||||
let oauthToken = try await client.continueOauthFlow(url: url)
|
||||
let client = Client(server: client.server, oauthToken: oauthToken)
|
||||
let client = MastodonClient(server: client.server, oauthToken: oauthToken)
|
||||
let account: Account = try await client.get(endpoint: Accounts.verifyCredentials)
|
||||
Telemetry.signal("account.added")
|
||||
appAccountsManager.add(
|
||||
|
|
|
@ -2,7 +2,7 @@ import AppAccount
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import NetworkClient
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
import Timeline
|
||||
|
|
|
@ -2,7 +2,7 @@ import Combine
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import NetworkClient
|
||||
import Observation
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
|
@ -31,7 +31,7 @@ struct DisplaySettingsView: View {
|
|||
|
||||
private let previewStatusViewModel = StatusRowViewModel(
|
||||
status: Status.placeholder(forSettings: true, language: "la"),
|
||||
client: Client(server: ""),
|
||||
client: MastodonClient(server: ""),
|
||||
routerPath: RouterPath()) // translate from latin button
|
||||
|
||||
var body: some View {
|
||||
|
@ -255,7 +255,6 @@ struct DisplaySettingsView: View {
|
|||
}
|
||||
}
|
||||
Toggle("settings.display.show-account-popover", isOn: $userPreferences.showAccountPopover)
|
||||
Toggle("Show Content Gradient", isOn: $theme.showContentGradient)
|
||||
Toggle("Compact Layout", isOn: $theme.compactLayoutPadding)
|
||||
}
|
||||
#if !os(visionOS)
|
||||
|
|
|
@ -17,9 +17,14 @@ struct IconSelectorView: View {
|
|||
}
|
||||
|
||||
case primary = 0
|
||||
case alt1, alt2, alt3, alt4, alt5, alt6, alt7, alt8, alt9, alt10, alt11, alt12, alt13, alt14,
|
||||
alt15
|
||||
case alt16, alt17, alt18, alt19, alt20, alt21
|
||||
case alt1, alt2
|
||||
|
||||
// Unused icons.
|
||||
case alt3, alt4, alt5, alt6, alt7, alt8
|
||||
case alt9, alt10, alt11, alt12, alt13
|
||||
case alt14, alt15, alt17, atl18, alt19
|
||||
|
||||
case alt16, alt20, alt21
|
||||
case alt22, alt23, alt24, alt25, alt26
|
||||
case alt27, alt28, alt29
|
||||
case alt30, alt31, alt32, alt33, alt34, alt35, alt36
|
||||
|
@ -48,10 +53,12 @@ struct IconSelectorView: View {
|
|||
IconSelector(
|
||||
title: "settings.app.icon.official".localized,
|
||||
icons: [
|
||||
.primary, .alt46, .alt1, .alt2, .alt3, .alt4,
|
||||
.alt5, .alt6, .alt7, .alt8,
|
||||
.alt9, .alt10, .alt11, .alt12, .alt13, .alt14, .alt15,
|
||||
.alt16, .alt17, .alt18, .alt19, .alt20, .alt21,
|
||||
.primary, .alt46, .alt1,
|
||||
]),
|
||||
IconSelector(
|
||||
title: "\("settings.app.icon.designed-by".localized) Erich Jurgens",
|
||||
icons: [
|
||||
.alt2
|
||||
]),
|
||||
IconSelector(
|
||||
title: "\("settings.app.icon.designed-by".localized) Albert Kinng",
|
||||
|
|
|
@ -2,7 +2,7 @@ import AppAccount
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import NetworkClient
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
import UserNotifications
|
||||
|
|
|
@ -4,7 +4,7 @@ import DesignSystem
|
|||
import Env
|
||||
import Foundation
|
||||
import Models
|
||||
import Network
|
||||
import NetworkClient
|
||||
import Nuke
|
||||
import SwiftData
|
||||
import SwiftUI
|
||||
|
@ -17,7 +17,7 @@ struct SettingsTabs: View {
|
|||
|
||||
@Environment(PushNotificationsService.self) private var pushNotifications
|
||||
@Environment(UserPreferences.self) private var preferences
|
||||
@Environment(Client.self) private var client
|
||||
@Environment(MastodonClient.self) private var client
|
||||
@Environment(CurrentInstance.self) private var currentInstance
|
||||
@Environment(AppAccountsManager.self) private var appAccountsManager
|
||||
@Environment(Theme.self) private var theme
|
||||
|
@ -39,8 +39,6 @@ struct SettingsTabs: View {
|
|||
accountsSection
|
||||
generalSection
|
||||
otherSections
|
||||
postStreamingSection
|
||||
AISection
|
||||
cacheSection
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
|
@ -49,7 +47,6 @@ struct SettingsTabs: View {
|
|||
#endif
|
||||
.navigationTitle(Text("settings.title"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||
.toolbar {
|
||||
if isModal {
|
||||
ToolbarItem {
|
||||
|
@ -147,7 +144,7 @@ struct SettingsTabs: View {
|
|||
if let token = account.oauthToken,
|
||||
let sub = pushNotifications.subscriptions.first(where: { $0.account.token == token })
|
||||
{
|
||||
let client = Client(server: account.server, oauthToken: token)
|
||||
let client = MastodonClient(server: account.server, oauthToken: token)
|
||||
await timelineCache.clearCache(for: client.id)
|
||||
await sub.deleteSubscription()
|
||||
appAccountsManager.delete(account: account)
|
||||
|
@ -190,12 +187,6 @@ struct SettingsTabs: View {
|
|||
NavigationLink(destination: TabbarEntriesSettingsView()) {
|
||||
Label("settings.general.tabbarEntries", systemImage: "platter.filled.bottom.iphone")
|
||||
}
|
||||
} else if UIDevice.current.userInterfaceIdiom == .pad
|
||||
|| UIDevice.current.userInterfaceIdiom == .mac
|
||||
{
|
||||
NavigationLink(destination: SidebarEntriesSettingsView()) {
|
||||
Label("settings.general.sidebarEntries", systemImage: "sidebar.squares.leading")
|
||||
}
|
||||
}
|
||||
NavigationLink(destination: TranslationSettingsView()) {
|
||||
Label("settings.general.translate", systemImage: "captions.bubble")
|
||||
|
@ -240,9 +231,6 @@ struct SettingsTabs: View {
|
|||
Toggle(isOn: $preferences.soundEffectEnabled) {
|
||||
Label("settings.other.sound-effect", systemImage: "hifispeaker")
|
||||
}
|
||||
Toggle(isOn: $preferences.fastRefreshEnabled) {
|
||||
Label("settings.other.fast-refresh", systemImage: "arrow.clockwise")
|
||||
}
|
||||
} header: {
|
||||
Text("settings.section.other")
|
||||
} footer: {
|
||||
|
@ -253,44 +241,6 @@ struct SettingsTabs: View {
|
|||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var postStreamingSection: some View {
|
||||
@Bindable var preferences = preferences
|
||||
Section {
|
||||
Toggle(isOn: $preferences.isPostsStreamingEnabled) {
|
||||
Label("Posts streaming", systemImage: "clock.badge")
|
||||
}
|
||||
} header: {
|
||||
Text("Streaming")
|
||||
} footer: {
|
||||
Text(
|
||||
"Enabling post streaming will automatically add new posts at the top of your home timeline. Disable if you get performance issues."
|
||||
)
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var AISection: some View {
|
||||
@Bindable var preferences = preferences
|
||||
Section {
|
||||
Toggle(isOn: $preferences.isOpenAIEnabled) {
|
||||
Label("settings.other.hide-openai", systemImage: "faxmachine")
|
||||
}
|
||||
} header: {
|
||||
Text("AI")
|
||||
} footer: {
|
||||
Text(
|
||||
"Disable to hide AI assisted tool options such as copywritting and alt-image description generated using AI. Uses OpenAI API. See our Privacy Policy for more information."
|
||||
)
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
private var appSection: some View {
|
||||
Section {
|
||||
#if !targetEnvironment(macCatalyst) && !os(visionOS)
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
import DesignSystem
|
||||
import Env
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
struct SidebarEntriesSettingsView: View {
|
||||
@Environment(Theme.self) private var theme
|
||||
@Environment(UserPreferences.self) private var userPreferences
|
||||
|
||||
@State private var sidebarTabs = SidebarTabs.shared
|
||||
|
||||
var body: some View {
|
||||
@Bindable var userPreferences = userPreferences
|
||||
Form {
|
||||
Section {
|
||||
ForEach($sidebarTabs.tabs, id: \.tab) { $tab in
|
||||
if tab.tab != .profile && tab.tab != .settings {
|
||||
Toggle(isOn: $tab.enabled) {
|
||||
tab.tab.label
|
||||
}
|
||||
}
|
||||
}
|
||||
.onMove(perform: move)
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
.environment(\.editMode, .constant(.active))
|
||||
.navigationTitle("settings.general.sidebarEntries")
|
||||
#if !os(visionOS)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
func move(from source: IndexSet, to destination: Int) {
|
||||
sidebarTabs.tabs.move(fromOffsets: source, toOffset: destination)
|
||||
}
|
||||
}
|
|
@ -52,13 +52,6 @@ struct TabbarEntriesSettingsView: View {
|
|||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
Section {
|
||||
Toggle("settings.display.show-tab-label", isOn: $userPreferences.showiPhoneTabLabel)
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
.navigationTitle("settings.general.tabbarEntries")
|
||||
#if !os(visionOS)
|
||||
|
|
|
@ -5,9 +5,11 @@ import Explore
|
|||
import Foundation
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
import Timeline
|
||||
import Env
|
||||
|
||||
@MainActor
|
||||
enum AppTab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
||||
enum AppTab: Identifiable, Hashable, CaseIterable, Codable {
|
||||
case timeline, notifications, mentions, explore, messages, settings, other
|
||||
case trending, federated, local
|
||||
case profile
|
||||
|
@ -17,9 +19,91 @@ enum AppTab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
|||
case followedTags
|
||||
case lists
|
||||
case links
|
||||
case anyTimelineFilter(filter: TimelineFilter)
|
||||
|
||||
nonisolated var id: Int {
|
||||
rawValue
|
||||
return switch self {
|
||||
case .timeline: 0
|
||||
case .notifications: 1
|
||||
case .mentions: 2
|
||||
case .explore: 3
|
||||
case .messages: 4
|
||||
case .settings: 5
|
||||
case .other: 6
|
||||
case .trending: 7
|
||||
case .federated: 8
|
||||
case .local: 9
|
||||
case .profile: 10
|
||||
case .bookmarks: 11
|
||||
case .favorites: 12
|
||||
case .post: 13
|
||||
case .followedTags: 14
|
||||
case .lists: 15
|
||||
case .links: 16
|
||||
case .anyTimelineFilter(let filter):
|
||||
filter.hashValue
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated static var allCases: [AppTab] {
|
||||
[.timeline,
|
||||
.notifications,
|
||||
.mentions,
|
||||
.explore,
|
||||
.messages,
|
||||
.settings,
|
||||
.other,
|
||||
.trending,
|
||||
.federated,
|
||||
.local,
|
||||
.profile,
|
||||
.bookmarks,
|
||||
.favorites,
|
||||
.post,
|
||||
.followedTags,
|
||||
.lists,
|
||||
.links]
|
||||
}
|
||||
|
||||
init(with id: Int) {
|
||||
switch id {
|
||||
case 0:
|
||||
self = .timeline
|
||||
case 1:
|
||||
self = .notifications
|
||||
case 2:
|
||||
self = .mentions
|
||||
case 3:
|
||||
self = .explore
|
||||
case 4:
|
||||
self = .messages
|
||||
case 5:
|
||||
self = .settings
|
||||
case 6:
|
||||
self = .other
|
||||
case 7:
|
||||
self = .trending
|
||||
case 8:
|
||||
self = .federated
|
||||
case 9:
|
||||
self = .local
|
||||
case 10:
|
||||
self = .profile
|
||||
case 11:
|
||||
self = .bookmarks
|
||||
case 12:
|
||||
self = .favorites
|
||||
case 13:
|
||||
self = .post
|
||||
case 14:
|
||||
self = .followedTags
|
||||
case 15:
|
||||
self = .lists
|
||||
case 16:
|
||||
self = .links
|
||||
default:
|
||||
self = .other
|
||||
}
|
||||
}
|
||||
|
||||
static func loggedOutTab() -> [AppTab] {
|
||||
|
@ -31,16 +115,22 @@ enum AppTab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
|||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeContentView(selectedTab: Binding<AppTab>) -> some View {
|
||||
func makeContentView(
|
||||
homeTimeline: Binding<TimelineFilter>,
|
||||
selectedTab: Binding<AppTab>,
|
||||
pinnedFilters: Binding<[TimelineFilter]>
|
||||
) -> some View {
|
||||
switch self {
|
||||
case let .anyTimelineFilter(filter):
|
||||
TimelineTab(timeline: .constant(filter))
|
||||
case .timeline:
|
||||
TimelineTab()
|
||||
TimelineTab(canFilterTimeline: true, timeline: homeTimeline, pinedFilters: pinnedFilters)
|
||||
case .trending:
|
||||
TimelineTab(timeline: .trending)
|
||||
TimelineTab(timeline: .constant(.trending))
|
||||
case .local:
|
||||
TimelineTab(timeline: .local)
|
||||
TimelineTab(timeline: .constant(.local))
|
||||
case .federated:
|
||||
TimelineTab(timeline: .federated)
|
||||
TimelineTab(timeline: .constant(.federated))
|
||||
case .notifications:
|
||||
NotificationsTab(selectedTab: selectedTab, lockedType: nil)
|
||||
case .mentions:
|
||||
|
@ -78,6 +168,15 @@ enum AppTab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
|||
}
|
||||
}
|
||||
|
||||
var tabPlacement: TabPlacement {
|
||||
switch self {
|
||||
case .timeline, .notifications, .explore, .links, .profile:
|
||||
return .pinned
|
||||
default:
|
||||
return .sidebarOnly
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var label: some View {
|
||||
if self != .other {
|
||||
|
@ -87,6 +186,8 @@ enum AppTab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
|||
|
||||
var title: LocalizedStringKey {
|
||||
switch self {
|
||||
case let .anyTimelineFilter(filter):
|
||||
filter.localizedTitle()
|
||||
case .timeline:
|
||||
"tab.timeline"
|
||||
case .trending:
|
||||
|
@ -126,6 +227,8 @@ enum AppTab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
|||
|
||||
var iconName: String {
|
||||
switch self {
|
||||
case let .anyTimelineFilter(filter):
|
||||
filter.iconName()
|
||||
case .timeline:
|
||||
"rectangle.stack"
|
||||
case .trending:
|
||||
|
@ -165,49 +268,63 @@ enum AppTab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
|||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
class SidebarTabs {
|
||||
struct SidedebarTab: Hashable, Codable {
|
||||
let tab: AppTab
|
||||
var enabled: Bool
|
||||
enum SidebarSections: Int, Identifiable {
|
||||
case timeline, activities, account, app, loggedOutTabs, iosTabs, visionOSTabs, lists, tags, localTimeline, tagGroup
|
||||
|
||||
nonisolated var id: Int {
|
||||
rawValue
|
||||
}
|
||||
|
||||
class Storage {
|
||||
@AppStorage("sidebar_tabs") var tabs: [SidedebarTab] = [
|
||||
.init(tab: .timeline, enabled: true),
|
||||
.init(tab: .trending, enabled: true),
|
||||
.init(tab: .federated, enabled: true),
|
||||
.init(tab: .local, enabled: true),
|
||||
.init(tab: .notifications, enabled: true),
|
||||
.init(tab: .mentions, enabled: true),
|
||||
.init(tab: .messages, enabled: true),
|
||||
.init(tab: .explore, enabled: true),
|
||||
.init(tab: .bookmarks, enabled: true),
|
||||
.init(tab: .favorites, enabled: true),
|
||||
.init(tab: .followedTags, enabled: true),
|
||||
.init(tab: .lists, enabled: true),
|
||||
.init(tab: .links, enabled: true),
|
||||
|
||||
.init(tab: .settings, enabled: true),
|
||||
.init(tab: .profile, enabled: true),
|
||||
]
|
||||
static var macOrIpadOSSections: [SidebarSections] {
|
||||
[.timeline, .activities, .account, .lists, .tags]
|
||||
}
|
||||
|
||||
private let storage = Storage()
|
||||
public static let shared = SidebarTabs()
|
||||
|
||||
var tabs: [SidedebarTab] {
|
||||
didSet {
|
||||
storage.tabs = tabs
|
||||
var title: String {
|
||||
switch self {
|
||||
case .timeline:
|
||||
"Timeline"
|
||||
case .activities:
|
||||
"Activities"
|
||||
case .account:
|
||||
"Account"
|
||||
case .app:
|
||||
"App"
|
||||
case .lists:
|
||||
"Lists"
|
||||
case .tags:
|
||||
"Followed Hashtags"
|
||||
case .localTimeline:
|
||||
"Local Timelines"
|
||||
case .tagGroup:
|
||||
"Tag Groups"
|
||||
case .loggedOutTabs, .iosTabs, .visionOSTabs:
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
func isEnabled(_ tab: AppTab) -> Bool {
|
||||
tabs.first(where: { $0.tab.id == tab.id })?.enabled == true
|
||||
var tabs: [AppTab] {
|
||||
switch self {
|
||||
case .timeline:
|
||||
return [.timeline, .trending, .local, .federated, .links, .explore]
|
||||
case .activities:
|
||||
return [.notifications, .mentions, .messages]
|
||||
case .account:
|
||||
return [.profile, .bookmarks, .favorites]
|
||||
case .app:
|
||||
return [.settings]
|
||||
case .loggedOutTabs:
|
||||
return [.timeline, .settings]
|
||||
case .iosTabs:
|
||||
return iOSTabs.shared.tabs
|
||||
case .visionOSTabs:
|
||||
return AppTab.visionOSTab()
|
||||
case .lists:
|
||||
return CurrentAccount.shared.lists.map { .anyTimelineFilter(filter: .list(list: $0)) }
|
||||
case .tags:
|
||||
return CurrentAccount.shared.tags.map { .anyTimelineFilter(filter: .hashtag(tag: $0.name, accountId: nil)) }
|
||||
case .localTimeline, .tagGroup:
|
||||
return []
|
||||
}
|
||||
|
||||
private init() {
|
||||
tabs = storage.tabs
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -219,11 +336,11 @@ class iOSTabs {
|
|||
}
|
||||
|
||||
class Storage {
|
||||
@AppStorage(TabEntries.first.rawValue) var firstTab = AppTab.timeline
|
||||
@AppStorage(TabEntries.second.rawValue) var secondTab = AppTab.notifications
|
||||
@AppStorage(TabEntries.third.rawValue) var thirdTab = AppTab.explore
|
||||
@AppStorage(TabEntries.fourth.rawValue) var fourthTab = AppTab.links
|
||||
@AppStorage(TabEntries.fifth.rawValue) var fifthTab = AppTab.profile
|
||||
@AppStorage(TabEntries.first.rawValue) var firstTab = AppTab.timeline.id
|
||||
@AppStorage(TabEntries.second.rawValue) var secondTab = AppTab.notifications.id
|
||||
@AppStorage(TabEntries.third.rawValue) var thirdTab = AppTab.explore.id
|
||||
@AppStorage(TabEntries.fourth.rawValue) var fourthTab = AppTab.links.id
|
||||
@AppStorage(TabEntries.fifth.rawValue) var fifthTab = AppTab.profile.id
|
||||
}
|
||||
|
||||
private let storage = Storage()
|
||||
|
@ -235,39 +352,39 @@ class iOSTabs {
|
|||
|
||||
var firstTab: AppTab {
|
||||
didSet {
|
||||
storage.firstTab = firstTab
|
||||
storage.firstTab = firstTab.id
|
||||
}
|
||||
}
|
||||
|
||||
var secondTab: AppTab {
|
||||
didSet {
|
||||
storage.secondTab = secondTab
|
||||
storage.secondTab = secondTab.id
|
||||
}
|
||||
}
|
||||
|
||||
var thirdTab: AppTab {
|
||||
didSet {
|
||||
storage.thirdTab = thirdTab
|
||||
storage.thirdTab = thirdTab.id
|
||||
}
|
||||
}
|
||||
|
||||
var fourthTab: AppTab {
|
||||
didSet {
|
||||
storage.fourthTab = fourthTab
|
||||
storage.fourthTab = fourthTab.id
|
||||
}
|
||||
}
|
||||
|
||||
var fifthTab: AppTab {
|
||||
didSet {
|
||||
storage.fifthTab = fifthTab
|
||||
storage.fifthTab = fifthTab.id
|
||||
}
|
||||
}
|
||||
|
||||
private init() {
|
||||
firstTab = storage.firstTab
|
||||
secondTab = storage.secondTab
|
||||
thirdTab = storage.thirdTab
|
||||
fourthTab = storage.fourthTab
|
||||
fifthTab = storage.fifthTab
|
||||
firstTab = .init(with: storage.firstTab)
|
||||
secondTab = .init(with: storage.secondTab)
|
||||
thirdTab = .init(with: storage.thirdTab)
|
||||
fourthTab = .init(with: storage.fourthTab)
|
||||
fifthTab = .init(with: storage.fifthTab)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import Combine
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import NetworkClient
|
||||
import NukeUI
|
||||
import SFSafeSymbols
|
||||
import SwiftData
|
||||
|
|
|
@ -2,7 +2,7 @@ import Combine
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import NetworkClient
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
|
||||
|
@ -72,7 +72,7 @@ struct AddRemoteTimelineView: View {
|
|||
instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
|
||||
) { newValue in
|
||||
Task {
|
||||
let client = Client(server: newValue)
|
||||
let client = MastodonClient(server: newValue)
|
||||
instance = try? await client.get(endpoint: Instances.instance)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import Combine
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import NetworkClient
|
||||
import SwiftData
|
||||
import SwiftUI
|
||||
import Timeline
|
||||
|
@ -16,24 +16,29 @@ struct TimelineTab: View {
|
|||
@Environment(Theme.self) private var theme
|
||||
@Environment(CurrentAccount.self) private var currentAccount
|
||||
@Environment(UserPreferences.self) private var preferences
|
||||
@Environment(Client.self) private var client
|
||||
@Environment(MastodonClient.self) private var client
|
||||
@State private var routerPath = RouterPath()
|
||||
|
||||
@State private var didAppear: Bool = false
|
||||
@State private var timeline: TimelineFilter = .home
|
||||
@State private var selectedTagGroup: TagGroup?
|
||||
|
||||
@Binding var timeline: TimelineFilter
|
||||
@Binding var pinnedFilters: [TimelineFilter]
|
||||
|
||||
@AppStorage("last_timeline_filter") var lastTimelineFilter: TimelineFilter = .home
|
||||
|
||||
@Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline]
|
||||
@Query(sort: \TagGroup.creationDate, order: .reverse) var tagGroups: [TagGroup]
|
||||
|
||||
@AppStorage("last_timeline_filter") var lastTimelineFilter: TimelineFilter = .home
|
||||
@AppStorage("timeline_pinned_filters") private var pinnedFilters: [TimelineFilter] = []
|
||||
|
||||
private let canFilterTimeline: Bool
|
||||
|
||||
init(timeline: TimelineFilter? = nil) {
|
||||
canFilterTimeline = timeline == nil
|
||||
_timeline = .init(initialValue: timeline ?? .home)
|
||||
init(
|
||||
canFilterTimeline: Bool = false, timeline: Binding<TimelineFilter>,
|
||||
pinedFilters: Binding<[TimelineFilter]> = .constant([])
|
||||
) {
|
||||
self.canFilterTimeline = canFilterTimeline
|
||||
_timeline = timeline
|
||||
_pinnedFilters = pinedFilters
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
@ -49,7 +54,6 @@ struct TimelineTab: View {
|
|||
.toolbar {
|
||||
toolbarView
|
||||
}
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||
.id(client.id)
|
||||
}
|
||||
.onAppear {
|
||||
|
@ -83,7 +87,7 @@ struct TimelineTab: View {
|
|||
lastTimelineFilter = newValue
|
||||
}
|
||||
switch newValue {
|
||||
case let .tagGroup(title, _, _):
|
||||
case .tagGroup(let title, _, _):
|
||||
if let group = tagGroups.first(where: { $0.title == title }) {
|
||||
selectedTagGroup = group
|
||||
}
|
||||
|
@ -149,16 +153,23 @@ struct TimelineTab: View {
|
|||
}
|
||||
}
|
||||
switch timeline {
|
||||
case let .list(list):
|
||||
ToolbarItem {
|
||||
case .list(let list):
|
||||
if #available(iOS 26.0, *) {
|
||||
ToolbarSpacer(placement: .topBarTrailing)
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
routerPath.presentedSheet = .listEdit(list: list)
|
||||
} label: {
|
||||
Image(systemName: "list.bullet")
|
||||
.foregroundStyle(theme.labelColor)
|
||||
}
|
||||
}
|
||||
case let .remoteLocal(server, _):
|
||||
ToolbarItem {
|
||||
case .remoteLocal(let server, _):
|
||||
if #available(iOS 26.0, *) {
|
||||
ToolbarSpacer(placement: .topBarTrailing)
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Menu {
|
||||
ForEach(RemoteTimelineFilter.allCases, id: \.self) { filter in
|
||||
Button {
|
||||
|
@ -169,6 +180,7 @@ struct TimelineTab: View {
|
|||
}
|
||||
} label: {
|
||||
Image(systemName: "line.3.horizontal.decrease.circle")
|
||||
.foregroundStyle(theme.labelColor)
|
||||
}
|
||||
}
|
||||
default:
|
||||
|
|
|
@ -9,41 +9,27 @@ struct ToolbarTab: ToolbarContent {
|
|||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
|
||||
@Environment(UserPreferences.self) private var userPreferences
|
||||
@Environment(Theme.self) private var theme
|
||||
|
||||
@Binding var routerPath: RouterPath
|
||||
|
||||
var body: some ToolbarContent {
|
||||
if !isSecondaryColumn {
|
||||
if horizontalSizeClass == .regular {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
if UIDevice.current.userInterfaceIdiom == .pad
|
||||
|| UIDevice.current.userInterfaceIdiom == .mac
|
||||
{
|
||||
Button {
|
||||
withAnimation {
|
||||
userPreferences.isSidebarExpanded.toggle()
|
||||
}
|
||||
} label: {
|
||||
if userPreferences.isSidebarExpanded {
|
||||
Image(systemName: "sidebar.squares.left")
|
||||
} else {
|
||||
Image(systemName: "sidebar.left")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
statusEditorToolbarItem(
|
||||
routerPath: routerPath,
|
||||
visibility: userPreferences.postVisibility)
|
||||
if UIDevice.current.userInterfaceIdiom != .pad
|
||||
|| (UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .compact)
|
||||
{
|
||||
if #available(iOS 26.0, *) {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
AppAccountsSelectorView(
|
||||
routerPath: routerPath,
|
||||
avatarConfig: theme.avatarShape == .circle ? .badge : .badgeRounded)
|
||||
avatarConfig: .embed)
|
||||
.offset(x: -12)
|
||||
}
|
||||
.sharedBackgroundVisibility(.hidden)
|
||||
} else {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
AppAccountsSelectorView(
|
||||
routerPath: routerPath,
|
||||
avatarConfig: .embed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 147 KiB |
Before Width: | Height: | Size: 147 KiB |
Before Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 156 KiB |
Before Width: | Height: | Size: 501 KiB |
|
@ -1,98 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIcon-fs8.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "dark.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"filename" : "tinted.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "32 1.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "64.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "256 1.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "512 1.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "Content.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 370 KiB |
Before Width: | Height: | Size: 545 KiB |
Before Width: | Height: | Size: 79 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIconAlternate0-image.imageset/AppIcon.png
vendored
Normal file
After Width: | Height: | Size: 3.2 MiB |
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIcon-fs8.png",
|
||||
"filename" : "AppIcon.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
|
|
Before Width: | Height: | Size: 156 KiB |
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIcon-fs8.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 85 KiB |
After Width: | Height: | Size: 3.3 MiB |
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"filename" : "AppIconAlternate1-iOS-Default-1024x1024@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
|
|
Before Width: | Height: | Size: 846 KiB |
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 151 KiB |
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIconAlternate10.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 565 KiB |
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIconAlternate10.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon15.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 120 KiB |
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon15.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 406 KiB |
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon16.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 108 KiB |
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon16.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 401 KiB |
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon17.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 72 KiB |
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon17.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 259 KiB |
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon18.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 69 KiB |
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon18.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 274 KiB |
Before Width: | Height: | Size: 102 KiB |
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Alternate43-fs8.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 268 KiB |
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Alternate43-fs8.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 165 KiB |
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Alternate44-fs8.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|