diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index b794ed5..c04725d 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -20,7 +20,7 @@ jobs: apt-get update apt-get install -y --no-install-recommends \ cmake g++ pkg-config python3 dpkg-dev libgtk-3-dev libcurl4-openssl-dev \ - libgstreamer1.0-dev gstreamer1.0-plugins-good libsecret-1-dev \ + libgstreamer1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good libsecret-1-dev \ libjson-glib-dev wtype xdotool - name: Configure run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release @@ -31,7 +31,7 @@ jobs: cmake --build build --target package --parallel 2 mkdir -p artifacts cp build/*.deb artifacts/ - sha256sum artifacts/*.deb > artifacts/SHA256SUMS + sha256sum artifacts/*.deb > artifacts/SHA256SUMS-amd64 - name: Attach packages to the Gitea release if: gitea.event_name == 'release' shell: bash @@ -63,7 +63,7 @@ jobs: apt-get update apt-get install -y --no-install-recommends \ cmake g++ pkg-config python3 dpkg-dev libgtk-3-dev libcurl4-openssl-dev \ - libgstreamer1.0-dev gstreamer1.0-plugins-good libsecret-1-dev \ + libgstreamer1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good libsecret-1-dev \ libjson-glib-dev wtype xdotool - name: Configure run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release @@ -74,7 +74,7 @@ jobs: cmake --build build --target package --parallel 2 mkdir -p artifacts cp build/*.deb artifacts/ - sha256sum artifacts/*.deb > artifacts/SHA256SUMS + sha256sum artifacts/*.deb > artifacts/SHA256SUMS-arm64 - name: Attach packages to the Gitea release if: gitea.event_name == 'release' shell: bash diff --git a/CMakeLists.txt b/CMakeLists.txt index ef7acfc..c3e26f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,6 @@ set(CPACK_PACKAGE_NAME "transui") set(CPACK_PACKAGE_CONTACT "Transui contributors") set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Transui contributors") set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) -set(CPACK_DEBIAN_PACKAGE_DEPENDS "gstreamer1.0-plugins-good, wtype | xdotool") +set(CPACK_DEBIAN_PACKAGE_DEPENDS "gstreamer1.0-plugins-base, gstreamer1.0-plugins-good, wtype | xdotool") set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) include(CPack) diff --git a/README.md b/README.md index 9182135..a2af95d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ On Debian/Ubuntu install the following packages yourself before compiling: ```sh sudo apt install cmake g++ pkg-config libgtk-3-dev libcurl4-openssl-dev \ - libgstreamer1.0-dev gstreamer1.0-plugins-good libsecret-1-dev \ + libgstreamer1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good libsecret-1-dev \ libjson-glib-dev wtype xdotool ``` diff --git a/scripts/publish-release-assets.sh b/scripts/publish-release-assets.sh index a3c4683..5028c94 100755 --- a/scripts/publish-release-assets.sh +++ b/scripts/publish-release-assets.sh @@ -36,7 +36,8 @@ python3 -c 'import json,sys; [print(a["name"]) for a in json.load(sys.stdin).get shopt -s nullglob assets=("$artifact_dir"/*.deb) -[[ -f "$artifact_dir/SHA256SUMS" ]] && assets+=("$artifact_dir/SHA256SUMS") +checksums=("$artifact_dir"/SHA256SUMS*) +assets+=("${checksums[@]}") ((${#assets[@]} > 0)) || { echo "No release assets found in $artifact_dir" >&2 exit 1 diff --git a/src/main.cpp b/src/main.cpp index 1fdb260..86b04a3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -22,7 +22,8 @@ constexpr const char* kConfigGroup = "transui"; const SecretSchema kSecretSchema = { "io.github.transui.credentials", SECRET_SCHEMA_NONE, - {{"scope", SECRET_SCHEMA_ATTRIBUTE_STRING}, {nullptr, SECRET_SCHEMA_ATTRIBUTE_STRING}}}; + {{"scope", SECRET_SCHEMA_ATTRIBUTE_STRING}, {nullptr, SECRET_SCHEMA_ATTRIBUTE_STRING}}, + 0, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}; struct Config { std::string hosted_url; @@ -46,6 +47,7 @@ struct App { GstElement* pipeline = nullptr; std::string recording_path; std::atomic cancelled{false}; + bool open_settings_requested = false; }; struct SettingsFields { @@ -170,6 +172,7 @@ bool save_config(App* app, const Config& config, const std::string& hosted_key, GtkWidget* labelled(GtkWidget* grid, const char* text, GtkWidget* control, int row) { GtkWidget* label = gtk_label_new(text); + gtk_style_context_add_class(gtk_widget_get_style_context(label), "field-label"); gtk_label_set_xalign(GTK_LABEL(label), 0); gtk_widget_set_valign(label, GTK_ALIGN_CENTER); gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1); @@ -207,17 +210,20 @@ void open_settings(App* app) { } GtkWidget* window = gtk_application_window_new(app->gtk_app); app->settings = window; + gtk_style_context_add_class(gtk_widget_get_style_context(window), "settings-window"); gtk_window_set_title(GTK_WINDOW(window), "Transui settings"); gtk_window_set_default_size(GTK_WINDOW(window), 590, 0); gtk_window_set_resizable(GTK_WINDOW(window), false); GtkWidget* root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_container_add(GTK_CONTAINER(window), root); GtkWidget* header = gtk_header_bar_new(); + gtk_style_context_add_class(gtk_widget_get_style_context(header), "transui-header"); gtk_header_bar_set_show_close_button(GTK_HEADER_BAR(header), true); gtk_header_bar_set_title(GTK_HEADER_BAR(header), "Transui"); gtk_window_set_titlebar(GTK_WINDOW(window), header); GtkWidget* grid = gtk_grid_new(); + gtk_style_context_add_class(gtk_widget_get_style_context(grid), "settings-panel"); gtk_grid_set_row_spacing(GTK_GRID(grid), 12); gtk_grid_set_column_spacing(GTK_GRID(grid), 18); gtk_widget_set_margin_start(grid, 28); @@ -254,7 +260,7 @@ void open_settings(App* app) { gtk_widget_set_margin_end(actions, 28); gtk_widget_set_margin_bottom(actions, 24); gtk_widget_set_halign(actions, GTK_ALIGN_END); - GtkWidget* save = gtk_button_new_with_label("Save"); + GtkWidget* save = gtk_button_new_with_label("Save settings"); gtk_style_context_add_class(gtk_widget_get_style_context(save), "suggested-action"); gtk_box_pack_end(GTK_BOX(actions), save, false, false, 0); gtk_box_pack_start(GTK_BOX(root), actions, false, false, 0); @@ -265,6 +271,7 @@ void open_settings(App* app) { delete static_cast(p); }), fields, nullptr, G_CONNECT_AFTER); gtk_widget_show_all(window); + gtk_window_present(GTK_WINDOW(window)); } gboolean tick_elapsed(gpointer data) { @@ -319,6 +326,8 @@ bool transcription_request(const std::string& url, const std::string& key, part = curl_mime_addpart(mime); curl_mime_name(part, "file"); curl_mime_filedata(part, path.c_str()); + curl_mime_filename(part, "recording.wav"); + curl_mime_type(part, "audio/wav"); if (!prompt.empty()) { part = curl_mime_addpart(mime); curl_mime_name(part, "prompt"); @@ -468,15 +477,18 @@ void cancel_processing(GtkButton* button, gpointer data) { void show_processing(App* app) { GtkWidget* window = gtk_application_window_new(app->gtk_app); app->processing = window; + gtk_style_context_add_class(gtk_widget_get_style_context(window), "processing-window"); gtk_window_set_title(GTK_WINDOW(window), "Transui"); gtk_window_set_default_size(GTK_WINDOW(window), 310, 190); gtk_window_set_resizable(GTK_WINDOW(window), false); gtk_window_set_deletable(GTK_WINDOW(window), false); GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); + gtk_style_context_add_class(gtk_widget_get_style_context(box), "process-panel"); gtk_widget_set_halign(box, GTK_ALIGN_CENTER); gtk_widget_set_valign(box, GTK_ALIGN_CENTER); gtk_container_add(GTK_CONTAINER(window), box); GtkWidget* spinner = gtk_spinner_new(); + gtk_style_context_add_class(gtk_widget_get_style_context(spinner), "signal-spinner"); gtk_widget_set_size_request(spinner, 36, 36); gtk_spinner_start(GTK_SPINNER(spinner)); gtk_box_pack_start(GTK_BOX(box), spinner, false, false, 0); @@ -488,6 +500,7 @@ void show_processing(App* app) { g_signal_connect(cancel, "clicked", G_CALLBACK(cancel_processing), app); g_signal_connect(window, "destroy", G_CALLBACK(processing_closed), app); gtk_widget_show_all(window); + gtk_window_present(GTK_WINDOW(window)); } void stop_recording(GtkButton*, gpointer data) { @@ -510,7 +523,8 @@ void stop_recording(GtkButton*, gpointer data) { app->recorder = nullptr; app->cancelled.store(false); show_processing(app); - auto* job = new Job{app, app->recording_path, app->config, app->hosted_key, app->local_key}; + auto* job = new Job{app, app->recording_path, app->config, app->hosted_key, + app->local_key, false, {}, {}}; g_thread_unref(g_thread_new("transcription", job_thread, job)); } @@ -566,16 +580,20 @@ void start_recording(App* app) { } GtkWidget* window = gtk_application_window_new(app->gtk_app); app->recorder = window; + gtk_style_context_add_class(gtk_widget_get_style_context(window), "recorder-window"); gtk_window_set_title(GTK_WINDOW(window), "Transui"); gtk_window_set_default_size(GTK_WINDOW(window), 330, 250); gtk_window_set_resizable(GTK_WINDOW(window), false); GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); + gtk_style_context_add_class(gtk_widget_get_style_context(box), "record-panel"); gtk_widget_set_halign(box, GTK_ALIGN_CENTER); gtk_widget_set_valign(box, GTK_ALIGN_CENTER); gtk_container_add(GTK_CONTAINER(window), box); - GtkWidget* dot = gtk_label_new("●"); - gtk_style_context_add_class(gtk_widget_get_style_context(dot), "record-dot"); - gtk_box_pack_start(GTK_BOX(box), dot, false, false, 0); + GtkWidget* light = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_widget_set_size_request(light, 30, 30); + gtk_widget_set_halign(light, GTK_ALIGN_CENTER); + gtk_style_context_add_class(gtk_widget_get_style_context(light), "record-light"); + gtk_box_pack_start(GTK_BOX(box), light, false, false, 0); GtkWidget* state = gtk_label_new("RECORDING"); gtk_style_context_add_class(gtk_widget_get_style_context(state), "record-state"); gtk_box_pack_start(GTK_BOX(box), state, false, false, 0); @@ -595,19 +613,88 @@ void start_recording(App* app) { g_signal_connect(stop, "clicked", G_CALLBACK(stop_recording), app); g_signal_connect(window, "destroy", G_CALLBACK(recorder_closed), app); gtk_widget_show_all(window); + gtk_window_present(GTK_WINDOW(window)); } void install_css() { const char* css = R"CSS( - window { background-color: #111827; color: #edf1f7; } - entry, combobox button { background: #1e293b; color: #edf1f7; border: 1px solid #334155; border-radius: 5px; } - label { color: #cbd5e1; } - .record-dot { color: #fb7185; font-size: 30px; } - .record-state { color: #fb7185; font-weight: 800; letter-spacing: 2px; font-size: 12px; } - .elapsed { color: #f8fafc; font-family: monospace; font-size: 38px; font-weight: 700; } - .state-title { color: #f8fafc; font-size: 18px; font-weight: 700; } - button.suggested-action { background: #f97316; color: #111827; border-color: #f97316; font-weight: 700; } - button.suggested-action:hover { background: #fb923c; } + window { + background-color: #181b1a; + color: #eef0e8; + font-family: Cantarell, sans-serif; + } + headerbar.transui-header { + background: #202421; + color: #eef0e8; + border-bottom: 1px solid #3b413c; + box-shadow: none; + } + .settings-panel { + background-color: #202421; + border: 1px solid #3b413c; + border-radius: 8px; + padding: 20px; + } + .field-label { + color: #aeb7ae; + font-size: 12px; + font-weight: 700; + } + entry, combobox button { + background: #2a2f2b; + color: #eef0e8; + border: 1px solid #4a514b; + border-radius: 4px; + padding: 7px 9px; + } + entry:focus, combobox button:focus, button:focus { + border-color: #d4a44c; + box-shadow: 0 0 0 1px #d4a44c; + } + label { color: #d8ddd5; } + checkbutton { color: #d8ddd5; } + .record-panel, .process-panel { + background-color: #202421; + border: 1px solid #3b413c; + border-radius: 12px; + padding: 28px 36px; + } + .record-light { + background: #e64a3c; + border: 6px solid #552a25; + border-radius: 50%; + box-shadow: 0 0 0 1px #f17a6f; + } + .record-state { + color: #f17a6f; + font-weight: 800; + letter-spacing: 2px; + font-size: 11px; + } + .elapsed { + color: #eef0e8; + font-family: "DejaVu Sans Mono", monospace; + font-size: 42px; + font-weight: 700; + letter-spacing: 1px; + } + .state-title { color: #eef0e8; font-size: 17px; font-weight: 700; } + .signal-spinner { color: #d4a44c; } + button { + background: #2a2f2b; + color: #eef0e8; + border: 1px solid #4a514b; + border-radius: 4px; + padding: 7px 12px; + } + button:hover { background: #343a35; } + button.suggested-action { + background: #d4a44c; + color: #181b1a; + border-color: #d4a44c; + font-weight: 800; + } + button.suggested-action:hover { background: #e1b760; } )CSS"; GtkCssProvider* provider = gtk_css_provider_new(); gtk_css_provider_load_from_data(provider, css, -1, nullptr); @@ -616,7 +703,22 @@ void install_css() { g_object_unref(provider); } -void activate(GtkApplication*, gpointer data) { start_recording(static_cast(data)); } +void activate(GtkApplication*, gpointer data) { + auto* app = static_cast(data); + if (app->open_settings_requested) open_settings(app); + else start_recording(app); +} + +int command_line(GApplication* application, GApplicationCommandLine* command, gpointer data) { + auto* app = static_cast(data); + int argument_count = 0; + gchar** arguments = g_application_command_line_get_arguments(command, &argument_count); + app->open_settings_requested = + argument_count == 2 && std::strcmp(arguments[1], "settings") == 0; + g_strfreev(arguments); + g_application_activate(application); + return 0; +} } // namespace @@ -630,12 +732,12 @@ int main(int argc, char** argv) { curl_global_init(CURL_GLOBAL_DEFAULT); App app; load_config(&app); - app.gtk_app = gtk_application_new(kAppId, G_APPLICATION_DEFAULT_FLAGS); + app.gtk_app = gtk_application_new(kAppId, G_APPLICATION_HANDLES_COMMAND_LINE); g_signal_connect(app.gtk_app, "startup", G_CALLBACK(+[](GtkApplication*, gpointer) { install_css(); }), nullptr); - if (settings) g_signal_connect_swapped(app.gtk_app, "activate", G_CALLBACK(open_settings), &app); - else g_signal_connect(app.gtk_app, "activate", G_CALLBACK(activate), &app); + g_signal_connect(app.gtk_app, "activate", G_CALLBACK(activate), &app); + g_signal_connect(app.gtk_app, "command-line", G_CALLBACK(command_line), &app); const int result = g_application_run(G_APPLICATION(app.gtk_app), argc, argv); if (app.pipeline) close_recorder(&app); remove_recording(&app);