Skip to content

Commit ca049a9

Browse files
authored
Merge pull request #41 from mensadilabs/dev
merge
2 parents 57ecc2b + 7204d78 commit ca049a9

27 files changed

+1180
-228
lines changed

.github/FUNDING.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# These are supported funding model platforms
2+
3+
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4+
patreon: # Replace with a single Patreon username
5+
open_collective: # Replace with a single Open Collective username
6+
ko_fi: # Replace with a single Ko-fi username
7+
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8+
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9+
liberapay: # Replace with a single Liberapay username
10+
issuehunt: # Replace with a single IssueHunt username
11+
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12+
polar: # Replace with a single Polar username
13+
buy_me_a_coffee: zzpr69dnqtr # Replace with a single Buy Me a Coffee username
14+
thanks_dev: # Replace with a single thanks.dev username
15+
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# macOS metadata files
22
._*
33
CLAUDE.local.md
4-
.vscode/**
4+
.vscode/**
5+
.kiro/**

CHNAGELOG.md renamed to CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
version 1.0.7 Build 1
1+
# VERSION|1.0.11
2+
3+
BUFFIX| Fix bugs
4+
- Top shef portraits no longer do unexpected headstands or cartwheels. I Hope.
5+
- Hopefully it also won't crash. But you may see reduced image quality in top shelf.
6+
- Better error handling.
7+
- Changed color gradient, this is much better on the eyes, I think.
8+
9+
10+
# version 1.0.7 Build 1
211

312
🎬 Animated Thumbnails
413

Immich Gallery.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,7 @@
547547
"$(inherited)",
548548
"@executable_path/Frameworks",
549549
);
550-
MARKETING_VERSION = 1.0.10;
550+
MARKETING_VERSION = 1.0.11;
551551
PRODUCT_BUNDLE_IDENTIFIER = "com.sanketh.dev.Immich-Gallery";
552552
PRODUCT_NAME = "$(TARGET_NAME)";
553553
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -576,7 +576,7 @@
576576
"$(inherited)",
577577
"@executable_path/Frameworks",
578578
);
579-
MARKETING_VERSION = 1.0.10;
579+
MARKETING_VERSION = 1.0.11;
580580
PRODUCT_BUNDLE_IDENTIFIER = "com.sanketh.dev.Immich-Gallery";
581581
PRODUCT_NAME = "$(TARGET_NAME)";
582582
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -673,7 +673,7 @@
673673
"@executable_path/Frameworks",
674674
"@executable_path/../../Frameworks",
675675
);
676-
MARKETING_VERSION = 0.1;
676+
MARKETING_VERSION = 0.2;
677677
PRODUCT_BUNDLE_IDENTIFIER = "com.sanketh.dev.Immich-Gallery.TopShelfExtension";
678678
PRODUCT_NAME = "$(TARGET_NAME)";
679679
SKIP_INSTALL = YES;
@@ -699,7 +699,7 @@
699699
"@executable_path/Frameworks",
700700
"@executable_path/../../Frameworks",
701701
);
702-
MARKETING_VERSION = 0.1;
702+
MARKETING_VERSION = 0.2;
703703
PRODUCT_BUNDLE_IDENTIFIER = "com.sanketh.dev.Immich-Gallery.TopShelfExtension";
704704
PRODUCT_NAME = "$(TARGET_NAME)";
705705
SKIP_INSTALL = YES;

Immich Gallery/ContentView.swift

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ extension Notification.Name {
4343
}
4444

4545
struct ContentView: View {
46+
// Auto slideshow state
47+
@AppStorage(UserDefaultsKeys.autoSlideshowTimeout) private var autoSlideshowTimeout: Int = 0
48+
@State private var inactivityTimer: Timer? = nil
49+
@State private var lastInteractionDate = Date()
4650
@StateObject private var networkService = NetworkService()
4751
@StateObject private var authService: AuthenticationService
4852
@StateObject private var assetService: AssetService
@@ -71,7 +75,7 @@ struct ContentView: View {
7175
}
7276

7377
var body: some View {
74-
NavigationView {
78+
NavigationView {
7579
ZStack {
7680
if !authService.isAuthenticated {
7781
// Show sign-in view
@@ -130,14 +134,19 @@ struct ContentView: View {
130134
}
131135
.tag(TabName.settings.rawValue)
132136
}
133-
.onAppear {
137+
.onAppear {
134138
setDefaultTab()
135139
checkForAppUpdate()
140+
startInactivityTimer()
136141
}
137142
.onChange(of: selectedTab) { oldValue, newValue in
138143
searchTabHighlighted = false
139-
print("Tab changed from \(oldValue) to \(newValue)")
140-
} .id(refreshTrigger) // Force refresh when user switches
144+
resetInactivityTimer()
145+
}
146+
.onChange(of: autoSlideshowTimeout) { _, _ in
147+
startInactivityTimer()
148+
}
149+
.id(refreshTrigger) // Force refresh when user switches
141150
// .accentColor(.blue)
142151
}
143152
}
@@ -163,6 +172,10 @@ struct ContentView: View {
163172
}
164173
}
165174
}
175+
.contentShape(Rectangle())
176+
.simultaneousGesture(
177+
TapGesture().onEnded { resetInactivityTimer() }
178+
)
166179
.sheet(isPresented: $showWhatsNew) {
167180
WhatsNewView(onDismiss: {
168181
showWhatsNew = false
@@ -171,6 +184,27 @@ struct ContentView: View {
171184
}
172185
}
173186

187+
// MARK: - Inactivity Timer Logic
188+
private func startInactivityTimer() {
189+
inactivityTimer?.invalidate()
190+
inactivityTimer = nil
191+
if autoSlideshowTimeout > 0 {
192+
inactivityTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
193+
let elapsed = Date().timeIntervalSince(lastInteractionDate)
194+
if elapsed > Double(autoSlideshowTimeout * 60) {
195+
inactivityTimer?.invalidate()
196+
inactivityTimer = nil
197+
// Post notification to start auto slideshow
198+
NotificationCenter.default.post(name: NSNotification.Name(NotificationNames.startAutoSlideshow), object: nil)
199+
}
200+
}
201+
}
202+
}
203+
204+
private func resetInactivityTimer() {
205+
lastInteractionDate = Date()
206+
}
207+
174208
private func setDefaultTab() {
175209
switch defaultStartupTab {
176210
case "albums":

Immich Gallery/Services/AssetService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class AssetService: ObservableObject {
6363
}
6464

6565
func loadVideoURL(asset: ImmichAsset) async throws -> URL {
66-
guard asset.type == .video else { throw ImmichError.serverError }
66+
guard asset.type == .video else { throw ImmichError.clientError(400) }
6767
let endpoint = "/api/assets/\(asset.id)/video/playback"
6868
guard let url = URL(string: "\(networkService.baseURL)\(endpoint)") else {
6969
throw ImmichError.invalidURL

Immich Gallery/Services/AuthenticationService.swift

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,15 +161,32 @@ class AuthenticationService: ObservableObject {
161161
}
162162

163163
private func validateTokenIfNeeded() {
164-
guard isAuthenticated && !networkService.baseURL.isEmpty else { return }
164+
guard isAuthenticated && !networkService.baseURL.isEmpty else {
165+
print("AuthenticationService: Skipping token validation - not authenticated or no baseURL")
166+
return
167+
}
165168

166169
Task {
167170
do {
168171
try await fetchUserInfo()
169-
} catch {
170-
DispatchQueue.main.async {
171-
self.signOut()
172+
print("AuthenticationService: Token validation successful")
173+
} catch let error as ImmichError {
174+
print("AuthenticationService: Token validation failed with ImmichError: \(error)")
175+
176+
if error.shouldLogout {
177+
print("AuthenticationService: Logging out user due to authentication error: \(error)")
178+
DispatchQueue.main.async {
179+
self.signOut()
180+
}
181+
} else {
182+
print("AuthenticationService: Preserving authentication state despite error: \(error)")
183+
// For server/network errors, preserve authentication state
184+
// The user will see error messages in the UI but won't be logged out
172185
}
186+
} catch {
187+
print("AuthenticationService: Token validation failed with unexpected error: \(error)")
188+
// Handle unexpected errors conservatively - don't logout
189+
// This preserves user authentication state for unknown error types
173190
}
174191
}
175192
}

Immich Gallery/Services/NetworkService.swift

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Foundation
1010
/// Base networking service that handles HTTP requests and authentication
1111
class NetworkService: ObservableObject {
1212
// MARK: - Configuration
13-
@Published var baseURL: String = ""
13+
@Published var baseURL: String = "http://localhost:2283"
1414
@Published var accessToken: String?
1515

1616
private let session = URLSession.shared
@@ -114,21 +114,42 @@ class NetworkService: ObservableObject {
114114
}
115115
}
116116

117-
let (data, response) = try await session.data(for: request)
117+
let (data, response): (Data, URLResponse)
118+
do {
119+
(data, response) = try await session.data(for: request)
120+
} catch {
121+
print("NetworkService: Network error occurred: \(error)")
122+
// Handle network connectivity issues (timeouts, connection refused, DNS failures, etc.)
123+
throw ImmichError.networkError
124+
}
118125

119126
guard let httpResponse = response as? HTTPURLResponse else {
120127
print("NetworkService: Invalid HTTP response")
121-
throw ImmichError.serverError
128+
throw ImmichError.networkError
122129
}
123130

124131
print("NetworkService: Response status code: \(httpResponse.statusCode)")
125132

126133
guard httpResponse.statusCode == 200 else {
127-
print("NetworkService: Server error with status \(httpResponse.statusCode)")
134+
print("NetworkService: HTTP error with status \(httpResponse.statusCode)")
128135
if let responseString = String(data: data, encoding: .utf8) {
129136
print("NetworkService: Response body: \(responseString)")
130137
}
131-
throw ImmichError.serverError
138+
139+
// Classify HTTP status codes into appropriate ImmichError types
140+
switch httpResponse.statusCode {
141+
case 401:
142+
throw ImmichError.notAuthenticated
143+
case 403:
144+
throw ImmichError.forbidden
145+
case 500...599:
146+
throw ImmichError.serverError(httpResponse.statusCode)
147+
case 400...499:
148+
throw ImmichError.clientError(httpResponse.statusCode)
149+
default:
150+
// For any other status codes, treat as server error
151+
throw ImmichError.serverError(httpResponse.statusCode)
152+
}
132153
}
133154

134155
do {
@@ -157,11 +178,37 @@ class NetworkService: ObservableObject {
157178
var request = URLRequest(url: url)
158179
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
159180

160-
let (data, response) = try await session.data(for: request)
181+
let (data, response): (Data, URLResponse)
182+
do {
183+
(data, response) = try await session.data(for: request)
184+
} catch {
185+
print("NetworkService: Network error occurred in makeDataRequest: \(error)")
186+
// Handle network connectivity issues (timeouts, connection refused, DNS failures, etc.)
187+
throw ImmichError.networkError
188+
}
189+
190+
guard let httpResponse = response as? HTTPURLResponse else {
191+
print("NetworkService: Invalid HTTP response in makeDataRequest")
192+
throw ImmichError.networkError
193+
}
161194

162-
guard let httpResponse = response as? HTTPURLResponse,
163-
httpResponse.statusCode == 200 else {
164-
throw ImmichError.serverError
195+
guard httpResponse.statusCode == 200 else {
196+
print("NetworkService: HTTP error in makeDataRequest with status \(httpResponse.statusCode)")
197+
198+
// Classify HTTP status codes into appropriate ImmichError types
199+
switch httpResponse.statusCode {
200+
case 401:
201+
throw ImmichError.notAuthenticated
202+
case 403:
203+
throw ImmichError.forbidden
204+
case 500...599:
205+
throw ImmichError.serverError(httpResponse.statusCode)
206+
case 400...499:
207+
throw ImmichError.clientError(httpResponse.statusCode)
208+
default:
209+
// For any other status codes, treat as server error
210+
throw ImmichError.serverError(httpResponse.statusCode)
211+
}
165212
}
166213

167214
return data
@@ -178,21 +225,36 @@ enum HTTPMethod: String {
178225
}
179226

180227
enum ImmichError: Error, LocalizedError {
181-
case notAuthenticated
182-
case invalidURL
183-
case serverError
184-
case networkError
228+
case notAuthenticated // 401 - Invalid/expired token
229+
case forbidden // 403 - Access denied
230+
case invalidURL // Malformed URL
231+
case serverError(Int) // 5xx - Server issues
232+
case networkError // Network connectivity issues
233+
case clientError(Int) // 4xx (except 401/403)
234+
235+
var shouldLogout: Bool {
236+
switch self {
237+
case .notAuthenticated, .forbidden:
238+
return true
239+
case .serverError, .networkError, .invalidURL, .clientError:
240+
return false
241+
}
242+
}
185243

186244
var errorDescription: String? {
187245
switch self {
188246
case .notAuthenticated:
189247
return "Not authenticated. Please log in."
248+
case .forbidden:
249+
return "Access forbidden. Please check your permissions."
190250
case .invalidURL:
191251
return "Invalid URL"
192-
case .serverError:
193-
return "Server error occurred"
252+
case .serverError(let statusCode):
253+
return "Server error occurred (HTTP \(statusCode))"
194254
case .networkError:
195255
return "Network error occurred"
256+
case .clientError(let statusCode):
257+
return "Client error occurred (HTTP \(statusCode))"
196258
}
197259
}
198-
}
260+
}

Immich Gallery/Views/AssetGridView.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,9 @@ struct AssetGridView: View {
248248
}
249249
}
250250
}
251+
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name(NotificationNames.startAutoSlideshow))) { _ in
252+
startSlideshow()
253+
}
251254
}
252255

253256
private func loadAssets() {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import SwiftUI
2+
3+
struct AutoSlideshowTimeoutPicker: View {
4+
@Binding var timeout: Int
5+
let options: [Int] = [0, 1, 3, 5, 10, 15, 30, 60] // 0 = Off
6+
7+
var body: some View {
8+
Picker("Auto-Start After", selection: $timeout) {
9+
ForEach(options, id: \.self) { value in
10+
if value == 0 {
11+
Text("Off").tag(0)
12+
} else {
13+
Text("\(value) min").tag(value)
14+
}
15+
}
16+
}
17+
.pickerStyle(.menu)
18+
.frame(width: 300, alignment: .trailing)
19+
}
20+
}
21+
22+
23+
#Preview {
24+
@Previewable
25+
@State var timeout = 10
26+
return AutoSlideshowTimeoutPicker(timeout: $timeout)
27+
}

0 commit comments

Comments
 (0)