Merge: protobuf + sync client skeleton
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
@@ -48,6 +50,179 @@ kotlin {
|
|||||||
jvmToolchain(17)
|
jvmToolchain(17)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Protobuf / gRPC code generation via protoc ---
|
||||||
|
//
|
||||||
|
// The protobuf-gradle-plugin (0.9.6) does not yet support AGP 9, so we drive
|
||||||
|
// protoc directly. A custom task exposes DirectoryProperty outputs that the
|
||||||
|
// AGP Variant API can consume.
|
||||||
|
|
||||||
|
abstract class GenerateProtoTask : DefaultTask() {
|
||||||
|
@get:Inject
|
||||||
|
abstract val execOps: ExecOperations
|
||||||
|
@get:InputDirectory
|
||||||
|
abstract val protoSourceDir: DirectoryProperty
|
||||||
|
|
||||||
|
@get:InputFiles
|
||||||
|
abstract val protocFile: ConfigurableFileCollection
|
||||||
|
|
||||||
|
@get:InputFiles
|
||||||
|
abstract val grpcJavaPluginFile: ConfigurableFileCollection
|
||||||
|
|
||||||
|
@get:InputFiles
|
||||||
|
abstract val grpcKotlinPluginFile: ConfigurableFileCollection
|
||||||
|
|
||||||
|
/** Protobuf JAR containing well-known type .proto files (google/protobuf/*.proto). */
|
||||||
|
@get:InputFiles
|
||||||
|
abstract val protobufIncludesJar: ConfigurableFileCollection
|
||||||
|
|
||||||
|
/** Temp directory for extracting well-known proto includes. */
|
||||||
|
@get:OutputDirectory
|
||||||
|
abstract val includesExtractDir: DirectoryProperty
|
||||||
|
|
||||||
|
/** Java sources generated by protoc (messages). */
|
||||||
|
@get:OutputDirectory
|
||||||
|
abstract val javaOutputDir: DirectoryProperty
|
||||||
|
|
||||||
|
/** Kotlin sources generated by protoc (Kotlin DSL builders). */
|
||||||
|
@get:OutputDirectory
|
||||||
|
abstract val kotlinOutputDir: DirectoryProperty
|
||||||
|
|
||||||
|
/** Java sources generated by protoc-gen-grpc-java (service stubs). */
|
||||||
|
@get:OutputDirectory
|
||||||
|
abstract val grpcOutputDir: DirectoryProperty
|
||||||
|
|
||||||
|
/** Kotlin sources generated by protoc-gen-grpc-kotlin (coroutine stubs). */
|
||||||
|
@get:OutputDirectory
|
||||||
|
abstract val grpcKtOutputDir: DirectoryProperty
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
fun generate() {
|
||||||
|
val javaOut = javaOutputDir.get().asFile
|
||||||
|
val kotlinOut = kotlinOutputDir.get().asFile
|
||||||
|
val grpcOut = grpcOutputDir.get().asFile
|
||||||
|
val grpcKtOut = grpcKtOutputDir.get().asFile
|
||||||
|
|
||||||
|
listOf(javaOut, kotlinOut, grpcOut, grpcKtOut).forEach { it.mkdirs() }
|
||||||
|
|
||||||
|
// Extract well-known .proto files from the protobuf JAR.
|
||||||
|
val extractDir = includesExtractDir.get().asFile
|
||||||
|
extractDir.mkdirs()
|
||||||
|
val jar = protobufIncludesJar.singleFile
|
||||||
|
java.util.zip.ZipFile(jar).use { zip ->
|
||||||
|
zip.entries().asSequence()
|
||||||
|
.filter { it.name.endsWith(".proto") }
|
||||||
|
.forEach { entry ->
|
||||||
|
val outFile = File(extractDir, entry.name)
|
||||||
|
outFile.parentFile.mkdirs()
|
||||||
|
zip.getInputStream(entry).use { input ->
|
||||||
|
outFile.outputStream().use { output -> input.copyTo(output) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val protoc = protocFile.singleFile.also { it.setExecutable(true) }
|
||||||
|
val grpcJava = grpcJavaPluginFile.singleFile.also { it.setExecutable(true) }
|
||||||
|
val grpcKt = grpcKotlinPluginFile.singleFile.also { it.setExecutable(true) }
|
||||||
|
|
||||||
|
val protoDir = protoSourceDir.get().asFile
|
||||||
|
val protoFiles = protoDir.walkTopDown().filter { it.extension == "proto" }.toList()
|
||||||
|
if (protoFiles.isEmpty()) return
|
||||||
|
|
||||||
|
execOps.exec {
|
||||||
|
commandLine(
|
||||||
|
protoc.absolutePath,
|
||||||
|
"--proto_path=${protoDir.absolutePath}",
|
||||||
|
"--proto_path=${extractDir.absolutePath}",
|
||||||
|
"--java_out=lite:${javaOut.absolutePath}",
|
||||||
|
"--kotlin_out=lite:${kotlinOut.absolutePath}",
|
||||||
|
"--plugin=protoc-gen-grpc=${grpcJava.absolutePath}",
|
||||||
|
"--grpc_out=lite:${grpcOut.absolutePath}",
|
||||||
|
"--plugin=protoc-gen-grpckt=${grpcKt.absolutePath}",
|
||||||
|
"--grpckt_out=lite:${grpcKtOut.absolutePath}",
|
||||||
|
*protoFiles.map { it.absolutePath }.toTypedArray(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val protobufIncludes: Configuration by configurations.creating {
|
||||||
|
isTransitive = false
|
||||||
|
}
|
||||||
|
val protocArtifact: Configuration by configurations.creating {
|
||||||
|
isTransitive = false
|
||||||
|
}
|
||||||
|
val grpcJavaPlugin: Configuration by configurations.creating {
|
||||||
|
isTransitive = false
|
||||||
|
}
|
||||||
|
val grpcKotlinPlugin: Configuration by configurations.creating {
|
||||||
|
isTransitive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the OS/arch classifier for native protoc binaries.
|
||||||
|
val protocClassifier: String = run {
|
||||||
|
val os = System.getProperty("os.name").lowercase()
|
||||||
|
val arch = System.getProperty("os.arch").lowercase()
|
||||||
|
val osName = when {
|
||||||
|
os.contains("mac") || os.contains("darwin") -> "osx"
|
||||||
|
os.contains("linux") -> "linux"
|
||||||
|
os.contains("windows") -> "windows"
|
||||||
|
else -> error("Unsupported OS: $os")
|
||||||
|
}
|
||||||
|
val archName = when {
|
||||||
|
arch.contains("aarch64") || arch.contains("arm64") -> "aarch_64"
|
||||||
|
arch.contains("amd64") || arch.contains("x86_64") -> "x86_64"
|
||||||
|
else -> error("Unsupported architecture: $arch")
|
||||||
|
}
|
||||||
|
"$osName-$archName"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
protobufIncludes("com.google.protobuf:protobuf-java:${libs.versions.protobuf.get()}")
|
||||||
|
protocArtifact("com.google.protobuf:protoc:${libs.versions.protobuf.get()}:$protocClassifier@exe")
|
||||||
|
grpcJavaPlugin("io.grpc:protoc-gen-grpc-java:${libs.versions.grpc.get()}:$protocClassifier@exe")
|
||||||
|
grpcKotlinPlugin("io.grpc:protoc-gen-grpc-kotlin:${libs.versions.grpcKotlin.get()}:jdk8@jar")
|
||||||
|
}
|
||||||
|
|
||||||
|
val protoGenBase = layout.buildDirectory.dir("generated/source/proto/main")
|
||||||
|
|
||||||
|
val generateProto by tasks.registering(GenerateProtoTask::class) {
|
||||||
|
description = "Generate Kotlin/gRPC sources from .proto files"
|
||||||
|
group = "build"
|
||||||
|
|
||||||
|
protoSourceDir.set(layout.projectDirectory.dir("src/main/proto"))
|
||||||
|
protocFile.from(protocArtifact)
|
||||||
|
grpcJavaPluginFile.from(grpcJavaPlugin)
|
||||||
|
grpcKotlinPluginFile.from(grpcKotlinPlugin)
|
||||||
|
|
||||||
|
protobufIncludesJar.from(protobufIncludes)
|
||||||
|
includesExtractDir.set(protoGenBase.map { it.dir("includes") })
|
||||||
|
|
||||||
|
javaOutputDir.set(protoGenBase.map { it.dir("java") })
|
||||||
|
kotlinOutputDir.set(protoGenBase.map { it.dir("kotlin") })
|
||||||
|
grpcOutputDir.set(protoGenBase.map { it.dir("grpc") })
|
||||||
|
grpcKtOutputDir.set(protoGenBase.map { it.dir("grpckt") })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register generated directories with AGP via the Variant API (AGP 9+).
|
||||||
|
androidComponents {
|
||||||
|
onVariants { variant ->
|
||||||
|
variant.sources.java?.addGeneratedSourceDirectory(
|
||||||
|
generateProto, GenerateProtoTask::javaOutputDir,
|
||||||
|
)
|
||||||
|
variant.sources.java?.addGeneratedSourceDirectory(
|
||||||
|
generateProto, GenerateProtoTask::grpcOutputDir,
|
||||||
|
)
|
||||||
|
variant.sources.kotlin?.addGeneratedSourceDirectory(
|
||||||
|
generateProto, GenerateProtoTask::kotlinOutputDir,
|
||||||
|
)
|
||||||
|
variant.sources.kotlin?.addGeneratedSourceDirectory(
|
||||||
|
generateProto, GenerateProtoTask::grpcKtOutputDir,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- end protobuf generation ---
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
@@ -70,6 +245,12 @@ dependencies {
|
|||||||
implementation(libs.coroutines.android)
|
implementation(libs.coroutines.android)
|
||||||
implementation(libs.reorderable)
|
implementation(libs.reorderable)
|
||||||
|
|
||||||
|
implementation(libs.protobuf.kotlin.lite)
|
||||||
|
implementation(libs.grpc.okhttp)
|
||||||
|
implementation(libs.grpc.protobuf.lite)
|
||||||
|
implementation(libs.grpc.stub)
|
||||||
|
implementation(libs.grpc.kotlin.stub)
|
||||||
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
testImplementation(libs.room.testing)
|
testImplementation(libs.room.testing)
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package net.metacircular.engpad.data.sync
|
||||||
|
|
||||||
|
import io.grpc.CallOptions
|
||||||
|
import io.grpc.Channel
|
||||||
|
import io.grpc.ClientCall
|
||||||
|
import io.grpc.ClientInterceptor
|
||||||
|
import io.grpc.ForwardingClientCall.SimpleForwardingClientCall
|
||||||
|
import io.grpc.ManagedChannel
|
||||||
|
import io.grpc.Metadata
|
||||||
|
import io.grpc.MethodDescriptor
|
||||||
|
import io.grpc.okhttp.OkHttpChannelBuilder
|
||||||
|
import net.metacircular.engpad.proto.v1.EngPadSyncGrpcKt
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.net.ssl.SSLContext
|
||||||
|
import javax.net.ssl.TrustManagerFactory
|
||||||
|
import java.security.KeyStore
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gRPC client for the eng-pad sync service.
|
||||||
|
*
|
||||||
|
* Uses OkHttp transport (suitable for Android) with TLS.
|
||||||
|
* Authentication credentials are sent as gRPC metadata on every call.
|
||||||
|
*/
|
||||||
|
class SyncClient(
|
||||||
|
host: String,
|
||||||
|
port: Int,
|
||||||
|
private val username: String,
|
||||||
|
private val password: String,
|
||||||
|
) {
|
||||||
|
private val channel: ManagedChannel = OkHttpChannelBuilder
|
||||||
|
.forAddress(host, port)
|
||||||
|
.useTransportSecurity()
|
||||||
|
.intercept(AuthInterceptor(username, password))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val stub: EngPadSyncGrpcKt.EngPadSyncCoroutineStub =
|
||||||
|
EngPadSyncGrpcKt.EngPadSyncCoroutineStub(channel)
|
||||||
|
|
||||||
|
fun shutdown() {
|
||||||
|
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val USERNAME_KEY: Metadata.Key<String> =
|
||||||
|
Metadata.Key.of("x-engpad-username", Metadata.ASCII_STRING_MARSHALLER)
|
||||||
|
|
||||||
|
private val PASSWORD_KEY: Metadata.Key<String> =
|
||||||
|
Metadata.Key.of("x-engpad-password", Metadata.ASCII_STRING_MARSHALLER)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interceptor that attaches username and password metadata to every outgoing call.
|
||||||
|
*/
|
||||||
|
private class AuthInterceptor(
|
||||||
|
private val username: String,
|
||||||
|
private val password: String,
|
||||||
|
) : ClientInterceptor {
|
||||||
|
override fun <ReqT, RespT> interceptCall(
|
||||||
|
method: MethodDescriptor<ReqT, RespT>,
|
||||||
|
callOptions: CallOptions,
|
||||||
|
next: Channel,
|
||||||
|
): ClientCall<ReqT, RespT> {
|
||||||
|
return object : SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) {
|
||||||
|
override fun start(responseListener: Listener<RespT>, headers: Metadata) {
|
||||||
|
headers.put(USERNAME_KEY, username)
|
||||||
|
headers.put(PASSWORD_KEY, password)
|
||||||
|
super.start(responseListener, headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package net.metacircular.engpad.data.sync
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
import com.google.protobuf.timestamp
|
||||||
|
import net.metacircular.engpad.data.db.NotebookDao
|
||||||
|
import net.metacircular.engpad.data.db.PageDao
|
||||||
|
import net.metacircular.engpad.data.db.StrokeDao
|
||||||
|
import net.metacircular.engpad.data.db.toFloatArray
|
||||||
|
import net.metacircular.engpad.proto.v1.DeleteNotebookRequestKt
|
||||||
|
import net.metacircular.engpad.proto.v1.ListNotebooksRequestKt
|
||||||
|
import net.metacircular.engpad.proto.v1.NotebookSummary
|
||||||
|
import net.metacircular.engpad.proto.v1.deleteNotebookRequest
|
||||||
|
import net.metacircular.engpad.proto.v1.listNotebooksRequest
|
||||||
|
import net.metacircular.engpad.proto.v1.notebook
|
||||||
|
import net.metacircular.engpad.proto.v1.page
|
||||||
|
import net.metacircular.engpad.proto.v1.stroke
|
||||||
|
import net.metacircular.engpad.proto.v1.syncNotebookRequest
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coordinates syncing local notebook data to the remote eng-pad sync service.
|
||||||
|
*/
|
||||||
|
class SyncManager(
|
||||||
|
private val client: SyncClient,
|
||||||
|
private val notebookDao: NotebookDao,
|
||||||
|
private val pageDao: PageDao,
|
||||||
|
private val strokeDao: StrokeDao,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Upload a notebook with all its pages and strokes to the sync server.
|
||||||
|
*/
|
||||||
|
suspend fun syncNotebook(notebookId: Long) {
|
||||||
|
val localNotebook = notebookDao.getById(notebookId)
|
||||||
|
?: throw IllegalArgumentException("Notebook $notebookId not found")
|
||||||
|
val localPages = pageDao.getByNotebookIdList(notebookId)
|
||||||
|
|
||||||
|
val protoNotebook = notebook {
|
||||||
|
id = localNotebook.id
|
||||||
|
title = localNotebook.title
|
||||||
|
pageSize = localNotebook.pageSize
|
||||||
|
createdAt = millisToTimestamp(localNotebook.createdAt)
|
||||||
|
updatedAt = millisToTimestamp(localNotebook.updatedAt)
|
||||||
|
lastPageId = localNotebook.lastPageId
|
||||||
|
for (localPage in localPages) {
|
||||||
|
val localStrokes = strokeDao.getByPageId(localPage.id)
|
||||||
|
pages += page {
|
||||||
|
id = localPage.id
|
||||||
|
notebookId = localPage.notebookId
|
||||||
|
pageNumber = localPage.pageNumber
|
||||||
|
createdAt = millisToTimestamp(localPage.createdAt)
|
||||||
|
for (localStroke in localStrokes) {
|
||||||
|
strokes += stroke {
|
||||||
|
id = localStroke.id
|
||||||
|
pageId = localStroke.pageId
|
||||||
|
penSize = localStroke.penSize
|
||||||
|
color = localStroke.color
|
||||||
|
pointData = ByteString.copyFrom(localStroke.pointData)
|
||||||
|
strokeOrder = localStroke.strokeOrder
|
||||||
|
createdAt = millisToTimestamp(localStroke.createdAt)
|
||||||
|
style = localStroke.style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val request = syncNotebookRequest {
|
||||||
|
notebook = protoNotebook
|
||||||
|
}
|
||||||
|
client.stub.syncNotebook(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a notebook on the sync server.
|
||||||
|
*/
|
||||||
|
suspend fun deleteNotebook(notebookId: Long) {
|
||||||
|
val request = deleteNotebookRequest {
|
||||||
|
this.notebookId = notebookId
|
||||||
|
}
|
||||||
|
client.stub.deleteNotebook(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all notebooks available on the sync server.
|
||||||
|
*/
|
||||||
|
suspend fun listRemoteNotebooks(): List<NotebookSummary> {
|
||||||
|
val request = listNotebooksRequest {}
|
||||||
|
val response = client.stub.listNotebooks(request)
|
||||||
|
return response.notebooksList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun millisToTimestamp(millis: Long) = timestamp {
|
||||||
|
seconds = millis / 1000
|
||||||
|
nanos = ((millis % 1000) * 1_000_000).toInt()
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
|
|
||||||
package engpad.v1;
|
package engpad.v1;
|
||||||
|
|
||||||
option go_package = "git.wntrmute.dev/kyle/eng-pad-server/gen/engpad/v1;engpadv1";
|
option go_package = "git.wntrmute.dev/kyle/engpad/gen/engpad/v1;engpadv1";
|
||||||
|
option java_package = "net.metacircular.engpad.proto.v1";
|
||||||
|
option java_multiple_files = true;
|
||||||
|
|
||||||
import "google/protobuf/timestamp.proto";
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
@@ -9,36 +12,13 @@ service EngPadSync {
|
|||||||
rpc SyncNotebook(SyncNotebookRequest) returns (SyncNotebookResponse);
|
rpc SyncNotebook(SyncNotebookRequest) returns (SyncNotebookResponse);
|
||||||
rpc DeleteNotebook(DeleteNotebookRequest) returns (DeleteNotebookResponse);
|
rpc DeleteNotebook(DeleteNotebookRequest) returns (DeleteNotebookResponse);
|
||||||
rpc ListNotebooks(ListNotebooksRequest) returns (ListNotebooksResponse);
|
rpc ListNotebooks(ListNotebooksRequest) returns (ListNotebooksResponse);
|
||||||
rpc CreateShareLink(CreateShareLinkRequest) returns (CreateShareLinkResponse);
|
|
||||||
rpc RevokeShareLink(RevokeShareLinkRequest) returns (RevokeShareLinkResponse);
|
|
||||||
rpc ListShareLinks(ListShareLinksRequest) returns (ListShareLinksResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message SyncNotebookRequest {
|
message SyncNotebookRequest {
|
||||||
int64 notebook_id = 1;
|
Notebook notebook = 1;
|
||||||
string title = 2;
|
|
||||||
string page_size = 3;
|
|
||||||
repeated PageData pages = 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message PageData {
|
message SyncNotebookResponse {}
|
||||||
int64 page_id = 1;
|
|
||||||
int32 page_number = 2;
|
|
||||||
repeated StrokeData strokes = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message StrokeData {
|
|
||||||
float pen_size = 1;
|
|
||||||
int32 color = 2;
|
|
||||||
string style = 3;
|
|
||||||
bytes point_data = 4;
|
|
||||||
int32 stroke_order = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SyncNotebookResponse {
|
|
||||||
int64 server_notebook_id = 1;
|
|
||||||
google.protobuf.Timestamp synced_at = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message DeleteNotebookRequest {
|
message DeleteNotebookRequest {
|
||||||
int64 notebook_id = 1;
|
int64 notebook_id = 1;
|
||||||
@@ -52,43 +32,39 @@ message ListNotebooksResponse {
|
|||||||
repeated NotebookSummary notebooks = 1;
|
repeated NotebookSummary notebooks = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message Notebook {
|
||||||
|
int64 id = 1;
|
||||||
|
string title = 2;
|
||||||
|
string page_size = 3;
|
||||||
|
google.protobuf.Timestamp created_at = 4;
|
||||||
|
google.protobuf.Timestamp updated_at = 5;
|
||||||
|
int64 last_page_id = 6;
|
||||||
|
repeated Page pages = 7;
|
||||||
|
}
|
||||||
|
|
||||||
message NotebookSummary {
|
message NotebookSummary {
|
||||||
int64 server_id = 1;
|
int64 id = 1;
|
||||||
int64 remote_id = 2;
|
string title = 2;
|
||||||
string title = 3;
|
string page_size = 3;
|
||||||
string page_size = 4;
|
google.protobuf.Timestamp updated_at = 4;
|
||||||
int32 page_count = 5;
|
int32 page_count = 5;
|
||||||
google.protobuf.Timestamp synced_at = 6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message CreateShareLinkRequest {
|
message Page {
|
||||||
int64 notebook_id = 1;
|
int64 id = 1;
|
||||||
int64 expires_in_seconds = 2;
|
int64 notebook_id = 2;
|
||||||
|
int32 page_number = 3;
|
||||||
|
google.protobuf.Timestamp created_at = 4;
|
||||||
|
repeated Stroke strokes = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CreateShareLinkResponse {
|
message Stroke {
|
||||||
string token = 1;
|
int64 id = 1;
|
||||||
string url = 2;
|
int64 page_id = 2;
|
||||||
google.protobuf.Timestamp expires_at = 3;
|
float pen_size = 3;
|
||||||
}
|
int32 color = 4;
|
||||||
|
bytes point_data = 5;
|
||||||
message RevokeShareLinkRequest {
|
int32 stroke_order = 6;
|
||||||
string token = 1;
|
google.protobuf.Timestamp created_at = 7;
|
||||||
}
|
string style = 8;
|
||||||
|
|
||||||
message RevokeShareLinkResponse {}
|
|
||||||
|
|
||||||
message ListShareLinksRequest {
|
|
||||||
int64 notebook_id = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ListShareLinksResponse {
|
|
||||||
repeated ShareLinkInfo links = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ShareLinkInfo {
|
|
||||||
string token = 1;
|
|
||||||
string url = 2;
|
|
||||||
google.protobuf.Timestamp created_at = 3;
|
|
||||||
google.protobuf.Timestamp expires_at = 4;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ core-ktx = "1.18.0"
|
|||||||
activity-compose = "1.13.0"
|
activity-compose = "1.13.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
reorderable = "3.0.0"
|
reorderable = "3.0.0"
|
||||||
|
protobuf = "4.29.3"
|
||||||
|
grpc = "1.69.0"
|
||||||
|
grpcKotlin = "1.4.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
|
||||||
@@ -38,6 +41,15 @@ coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-
|
|||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
reorderable = { group = "sh.calvin.reorderable", name = "reorderable", version.ref = "reorderable" }
|
reorderable = { group = "sh.calvin.reorderable", name = "reorderable", version.ref = "reorderable" }
|
||||||
|
|
||||||
|
protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf" }
|
||||||
|
protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" }
|
||||||
|
grpc-okhttp = { group = "io.grpc", name = "grpc-okhttp", version.ref = "grpc" }
|
||||||
|
grpc-protobuf-lite = { group = "io.grpc", name = "grpc-protobuf-lite", version.ref = "grpc" }
|
||||||
|
grpc-stub = { group = "io.grpc", name = "grpc-stub", version.ref = "grpc" }
|
||||||
|
grpc-kotlin-stub = { group = "io.grpc", name = "grpc-kotlin-stub", version.ref = "grpcKotlin" }
|
||||||
|
grpc-protoc-gen-java = { group = "io.grpc", name = "protoc-gen-grpc-java", version.ref = "grpc" }
|
||||||
|
grpc-protoc-gen-kotlin = { group = "io.grpc", name = "protoc-gen-grpc-kotlin", version.ref = "grpcKotlin" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
|
|||||||
Reference in New Issue
Block a user