Complete the MyIceK rename by updating the Kotlin package from ch.parano.myice to ch.parano.myicek across all source sets (androidMain, commonMain, desktopMain, iosMain, commonTest), the build.gradle.kts (namespace, applicationId, mainClass, desktop packageName) and the iOS Info.plist bundle URL name. The myice:// OAuth scheme is kept unchanged as it depends on the backend. Also add pull-to-refresh to ScheduleScreen using Material 3 PullToRefreshBox, replacing the manual loading overlay.
116 lines
4.3 KiB
Kotlin
116 lines
4.3 KiB
Kotlin
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// MyIce Kotlin Multiplatform — schedule/convocation viewer
|
|
// Copyright (C) 2026 parano.ch
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package ch.parano.myicek.auth
|
|
|
|
import kotlinx.coroutines.CompletableDeferred
|
|
import kotlinx.coroutines.TimeoutCancellationException
|
|
import kotlinx.coroutines.withTimeout
|
|
import java.awt.Desktop
|
|
import java.io.BufferedReader
|
|
import java.io.InputStreamReader
|
|
import java.io.PrintWriter
|
|
import java.net.InetSocketAddress
|
|
import java.net.ServerSocket
|
|
import java.net.URI
|
|
import java.net.URLEncoder
|
|
import kotlin.concurrent.thread
|
|
|
|
class DesktopOAuthClient : OAuthClient {
|
|
|
|
override fun redirectUri(): String = "myice://callback"
|
|
|
|
override suspend fun authenticate(loginUrl: String): String? {
|
|
val server = ServerSocket()
|
|
server.bind(InetSocketAddress("127.0.0.1", 0))
|
|
val serverPort = server.localPort
|
|
|
|
val realRedirectUri = "http://localhost:$serverPort/callback"
|
|
val fullLoginUrl = loginUrl.replace(
|
|
"redirect_uri=myice://callback",
|
|
"redirect_uri=" + URLEncoder.encode(realRedirectUri, "UTF-8")
|
|
)
|
|
|
|
val deferred = CompletableDeferred<String?>()
|
|
|
|
thread {
|
|
try {
|
|
while (true) {
|
|
val socket = server.accept()
|
|
val reader = BufferedReader(InputStreamReader(socket.getInputStream()))
|
|
val writer = PrintWriter(socket.getOutputStream())
|
|
|
|
val requestLine = reader.readLine() ?: continue
|
|
val path = requestLine.split(" ").getOrNull(1) ?: continue
|
|
|
|
if (path.startsWith("/callback")) {
|
|
val html = """
|
|
<html><body>
|
|
<script>
|
|
var hash = window.location.hash.substring(1);
|
|
window.location.href = "/token?" + hash;
|
|
</script>
|
|
</body></html>
|
|
""".trimIndent()
|
|
writer.print("HTTP/1.1 200 OK\r\n")
|
|
writer.print("Content-Type: text/html\r\n")
|
|
writer.print("Connection: close\r\n")
|
|
writer.print("\r\n")
|
|
writer.print(html)
|
|
writer.flush()
|
|
} else if (path.startsWith("/token")) {
|
|
val query = path.substringAfter("?")
|
|
val token = parseAccessTokenFromFragment(query)
|
|
deferred.complete(token)
|
|
|
|
writer.print("HTTP/1.1 200 OK\r\n")
|
|
writer.print("Content-Type: text/html\r\n")
|
|
writer.print("Connection: close\r\n")
|
|
writer.print("\r\n")
|
|
writer.print("<html><body><h1>Authentication complete. You can close this window.</h1></body></html>")
|
|
writer.flush()
|
|
socket.close()
|
|
break
|
|
}
|
|
socket.close()
|
|
}
|
|
} catch (e: Exception) {
|
|
deferred.complete(null)
|
|
} finally {
|
|
server.close()
|
|
}
|
|
}
|
|
|
|
try {
|
|
Desktop.getDesktop().browse(URI(fullLoginUrl))
|
|
} catch (e: Exception) {
|
|
server.close()
|
|
return null
|
|
}
|
|
|
|
return try {
|
|
withTimeout(5 * 60 * 1000L) {
|
|
deferred.await()
|
|
}
|
|
} catch (e: TimeoutCancellationException) {
|
|
null
|
|
} finally {
|
|
server.close()
|
|
}
|
|
}
|
|
}
|