Skip to content

Commit af9855e

Browse files
committed
more bug fixes, add AGENTS.md, linter fixes. Add animation for frequency tuning
1 parent 831c0a0 commit af9855e

File tree

5 files changed

+113
-30
lines changed

5 files changed

+113
-30
lines changed

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Core features:
3333
- Build: `meson setup build && meson compile -C build`
3434
- Reconfigure: `meson setup build --reconfigure`
3535
- Translation template: `ninja -C build com.k0vcz.Artemis-pot`
36+
- Lint must pass before commit (same expectation as pre-commit hooks, including `vala-lint`).
3637

3738
When changing UI/resources, ensure the build succeeds and generated resources remain valid.
3839

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Artemis is designed to be cross-platform, lightweight, and easy to use.
2929
- Supports serial, USB, and network-connected radios via Hamlib.
3030

3131
- **Import/Export**
32-
- Import your already hunted parks from (POTA.app)[https://pota.app]
32+
- Import your already hunted parks from [POTA.app](https://pota.app)
3333
- Ability to exporting hunter QSOs to QRZ; LoTW, UDP, or local ADIF log coming soon.
3434

3535
- **UI**

src/map_view.vala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -287,12 +287,12 @@ public class MapView : Gtk.Box {
287287
if (now.compare (expires) > 0)
288288
return false;
289289

290-
if ((Application.current_program_filter != null) &&
290+
if ((Application.current_program_filter != null) &&
291291
(Application.current_program_filter != _ ("All")) &&
292292
!spot.park_ref.down ().has_prefix (Application.current_program_filter.down ()))
293293
return false;
294294

295-
if ((Application.current_mode_filter != null) &&
295+
if ((Application.current_mode_filter != null) &&
296296
(Application.current_mode_filter != _ ("All")) &&
297297
!spot.mode.down ().contains (Application.current_mode_filter.down ()))
298298
return false;
@@ -372,10 +372,10 @@ public class MapView : Gtk.Box {
372372
click.pressed.connect (() => {
373373
marker_clicked = true;
374374
Application.current_spot_hash = spot.hash;
375-
375+
376376
var sidebar_box = split_view.sidebar as Gtk.Box;
377377
var spot_card = new SpotCard.from_spot (spot);
378-
378+
379379
for (var child = sidebar_box.get_first_child (); child != null;) {
380380
sidebar_box.remove (child);
381381
child = child.get_next_sibling ();
@@ -429,7 +429,7 @@ public class MapView : Gtk.Box {
429429

430430
map_widget.insert_layer_above (marker_layer, map_layer);
431431

432-
if (Application.current_spot_hash == BLANK_HASH ||
432+
if (Application.current_spot_hash == BLANK_HASH ||
433433
!valid_hashes.contains (Application.current_spot_hash)) {
434434
if (split_view.show_sidebar) {
435435
split_view.show_sidebar = false;

src/repo.vala

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public class CallsignCacheEntry : Object {
1515

1616
public class CallsignCache : Object {
1717
private HashTable<string, CallsignCacheEntry> ham_cache;
18+
private HashSet<string> avatar_fetch_inflight;
1819
private Soup.Session avatar_session;
1920
public uint ttl_seconds { get; construct; default = 3600; }
2021

@@ -34,13 +35,14 @@ public class CallsignCache : Object {
3435
construct {
3536
ham_cache = new HashTable<string, CallsignCacheEntry> (GLib.str_hash,
3637
GLib.str_equal);
38+
avatar_fetch_inflight = new HashSet<string> ();
3739
avatar_session = new Soup.Session ();
3840
var cache_dir = Path.build_filename (Environment.get_user_cache_dir (),
3941
"artemis");
4042
var cache = new Soup.Cache (cache_dir, Soup.CacheType.SINGLE_USER);
4143
cache.set_max_size (50 * 1024 * 1024);
4244
avatar_session.add_feature (cache);
43-
avatar_session.timeout = 10;
45+
avatar_session.timeout = 3;
4446
avatar_session.user_agent = "Artemis/0.1.0";
4547
}
4648

@@ -52,6 +54,7 @@ public class CallsignCache : Object {
5254

5355
public void clear () {
5456
ham_cache.remove_all ();
57+
avatar_fetch_inflight.clear ();
5558
}
5659

5760
public async void load_callsigns (HashSet<string> callsigns) {
@@ -70,29 +73,34 @@ public class CallsignCache : Object {
7073
if ((cached_entry != null) && (cached_entry.avatar != null))
7174
return cached_entry.avatar;
7275

76+
if (avatar_fetch_inflight.contains (callsign))
77+
return null;
78+
79+
avatar_fetch_inflight.add (callsign);
80+
Gdk.Texture? avatar = null;
7381
try {
7482
var gravatar_hash = entry.gravatar_hash;
75-
if ((gravatar_hash == null) || (gravatar_hash.strip () == ""))
76-
return null;
77-
78-
var url = "https://www.gravatar.com/avatar/%s?s=128&d=identicon"
79-
.printf (gravatar_hash);
83+
if ((gravatar_hash != null) && (gravatar_hash.strip () != "")) {
84+
var url = "https://www.gravatar.com/avatar/%s?s=128&d=identicon"
85+
.printf (gravatar_hash);
8086

81-
var message = new Soup.Message ("GET", url);
87+
var message = new Soup.Message ("GET", url);
8288

83-
var stream = yield avatar_session.send_async (message, GLib.Priority.
84-
DEFAULT, null);
89+
var stream = yield avatar_session.send_async (message, GLib.Priority.
90+
DEFAULT, null);
8591

86-
var pixbuf = new Gdk.Pixbuf.from_stream (stream);
87-
if (pixbuf != null) {
88-
var texture = Gdk.Texture.for_pixbuf (pixbuf);
89-
cached_entry.avatar = texture;
90-
return texture;
92+
var pixbuf = new Gdk.Pixbuf.from_stream (stream);
93+
if (pixbuf != null) {
94+
var texture = Gdk.Texture.for_pixbuf (pixbuf);
95+
cached_entry.avatar = texture;
96+
avatar = texture;
97+
}
9198
}
9299
} catch (Error e) {
93100
warning ("Failed to fetch avatar for %s: %s", callsign, e.message);
94101
}
95-
return null;
102+
avatar_fetch_inflight.remove (callsign);
103+
return avatar;
96104
}
97105

98106
public async Activator? get_callsign (string callsign) {

src/window.vala

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,18 @@ public sealed class AppWindow : Gtk.Window {
7272

7373
private uint timer_id = 0;
7474
private uint progress_timer_id = 0;
75+
private uint radio_vfo_anim_id = 0;
7576
private int64 last_refresh_time = 0;
77+
private int64 radio_vfo_anim_started_at = 0;
7678

7779
private uint current_ticks = 0;
7880
private bool update_paused = false;
7981
private ArrayList<Adw.ViewStackPage> band_pages;
8082
private MapView map_view;
83+
private bool has_displayed_radio_vfo = false;
84+
private int displayed_radio_vfo_khz = 0;
85+
private int radio_vfo_anim_start_khz = 0;
86+
private int radio_vfo_anim_target_khz = 0;
8187

8288
private ulong program_select_handler = 0;
8389

@@ -118,7 +124,7 @@ public sealed class AppWindow : Gtk.Window {
118124
search_entry.search_changed.connect (() => {
119125
Application.current_search_text = search_entry.text;
120126

121-
map_view.bounce_filter ();
127+
bounce_map_filter_if_ready ();
122128
foreach (var page in band_pages) {
123129
var band_view = page.get_child () as BandView;
124130
band_view.bounce_filter ();
@@ -140,7 +146,7 @@ public sealed class AppWindow : Gtk.Window {
140146

141147
Application.current_mode_filter = mode;
142148

143-
map_view.bounce_filter ();
149+
bounce_map_filter_if_ready ();
144150

145151
foreach (var page in band_pages) {
146152
var band_view = page.get_child () as BandView;
@@ -156,7 +162,10 @@ public sealed class AppWindow : Gtk.Window {
156162

157163
Application.spot_repo.busy_changed.connect ((busy) => {
158164
loading_spinner.visible = busy;
159-
program_select.disconnect (program_select_handler);
165+
if (busy && (program_select_handler != 0)) {
166+
program_select.disconnect (program_select_handler);
167+
program_select_handler = 0;
168+
}
160169
});
161170

162171
Application.spot_repo.refreshed.connect ((spots_updated) => {
@@ -179,8 +188,10 @@ public sealed class AppWindow : Gtk.Window {
179188
}
180189
}
181190
program_select.set_selected (idx);
182-
program_select_handler = program_select.notify["selected"].connect (
183-
on_program_selected);
191+
if (program_select_handler == 0) {
192+
program_select_handler = program_select.notify["selected"].connect (
193+
on_program_selected);
194+
}
184195

185196
last_refresh_time = get_monotonic_time ();
186197
current_ticks = 0;
@@ -228,7 +239,7 @@ public sealed class AppWindow : Gtk.Window {
228239
band_stack.notify["visible-child-name"].connect (() => {
229240
Application.current_band_filter = band_stack.visible_child_name;
230241
update_status_bar ();
231-
map_view.bounce_filter ();
242+
bounce_map_filter_if_ready ();
232243
});
233244

234245
var model = search_select.get_model () as Gtk.StringList;
@@ -292,11 +303,16 @@ public sealed class AppWindow : Gtk.Window {
292303
(uint)total_visible
293304
).printf ((uint)total_visible);
294305

295-
var status_bar_filtered_text = "; %u filtered".printf ((uint)filtered_count);
306+
var status_bar_filtered_text = " %u filtered".printf ((uint)filtered_count);
296307

297308
status_bar_text.label = "%s%s".printf (status_bar_spots_text, status_bar_filtered_text);
298309
}
299310

311+
private void bounce_map_filter_if_ready () {
312+
if (map_view != null)
313+
map_view.bounce_filter ();
314+
}
315+
300316
private void initial_update () {
301317
Application.spot_repo.update_spots.begin ((obj, res) => {
302318
Application.spot_repo.update_spots.end (res);
@@ -337,8 +353,60 @@ public sealed class AppWindow : Gtk.Window {
337353
}
338354
}
339355

356+
private void stop_radio_vfo_animation () {
357+
if (radio_vfo_anim_id != 0) {
358+
Source.remove (radio_vfo_anim_id);
359+
radio_vfo_anim_id = 0;
360+
}
361+
}
362+
363+
private void set_radio_vfo_label_animated (int freq_khz) {
364+
if (!has_displayed_radio_vfo) {
365+
displayed_radio_vfo_khz = freq_khz;
366+
has_displayed_radio_vfo = true;
367+
radio_vfo.label = format_vfo (freq_khz);
368+
return;
369+
}
370+
371+
stop_radio_vfo_animation ();
372+
373+
radio_vfo_anim_start_khz = displayed_radio_vfo_khz;
374+
radio_vfo_anim_target_khz = freq_khz;
375+
if (radio_vfo_anim_start_khz == radio_vfo_anim_target_khz) {
376+
radio_vfo.label = format_vfo (freq_khz);
377+
return;
378+
}
379+
380+
radio_vfo_anim_started_at = get_monotonic_time ();
381+
radio_vfo_anim_id = Timeout.add (16, () => {
382+
const double DURATION_MS = 160.0;
383+
var elapsed_ms = (get_monotonic_time () - radio_vfo_anim_started_at) / 1000.0;
384+
var t = elapsed_ms / DURATION_MS;
385+
if (t > 1.0)
386+
t = 1.0;
387+
388+
// Ease-out interpolation so the value settles smoothly.
389+
var eased_t = 1.0 - ((1.0 - t) * (1.0 - t));
390+
var interpolated = (double)radio_vfo_anim_start_khz +
391+
((double)(radio_vfo_anim_target_khz - radio_vfo_anim_start_khz) * eased_t);
392+
displayed_radio_vfo_khz = (int)Math.round (interpolated);
393+
radio_vfo.label = format_vfo (displayed_radio_vfo_khz);
394+
395+
if (t >= 1.0) {
396+
displayed_radio_vfo_khz = radio_vfo_anim_target_khz;
397+
radio_vfo.label = format_vfo (displayed_radio_vfo_khz);
398+
radio_vfo_anim_id = 0;
399+
return Source.REMOVE;
400+
}
401+
402+
return Source.CONTINUE;
403+
});
404+
}
405+
340406
private void power_off_radio () {
341407
Application.radio_control.disconnect ().disown ();
408+
stop_radio_vfo_animation ();
409+
has_displayed_radio_vfo = false;
342410
radio_vfo.label = _("Radio disconnected");
343411
radio_mode.visible = false;
344412
}
@@ -367,16 +435,20 @@ public sealed class AppWindow : Gtk.Window {
367435
Application.radio_control.radio_status.connect ((freq, mode) => {
368436
if (freq > 0 && mode != 0) {
369437
radio_mode.visible = true;
370-
radio_vfo.label = format_vfo (freq);
438+
set_radio_vfo_label_animated (freq);
371439
radio_mode.label = RadioControl.mode_string (mode);
372440
radio_power_button.active = true;
373441
} else {
442+
stop_radio_vfo_animation ();
443+
has_displayed_radio_vfo = false;
374444
radio_mode.visible = false;
375445
radio_power_button.active = false;
376446
radio_vfo.label = _("Radio disconnected");
377447
}
378448
});
379449
} else {
450+
stop_radio_vfo_animation ();
451+
has_displayed_radio_vfo = false;
380452
radio_mode.visible = false;
381453
radio_power_button.active = false;
382454
radio_vfo.label = _("Radio disconnected");
@@ -473,7 +545,7 @@ public sealed class AppWindow : Gtk.Window {
473545

474546
Application.current_program_filter = program;
475547

476-
map_view.bounce_filter ();
548+
bounce_map_filter_if_ready ();
477549

478550
foreach (var page in band_pages) {
479551
var band_view = page.get_child () as BandView;
@@ -490,5 +562,7 @@ public sealed class AppWindow : Gtk.Window {
490562

491563
if (progress_timer_id != 0)
492564
Source.remove (progress_timer_id);
565+
566+
stop_radio_vfo_animation ();
493567
}
494568
} /* class AppWindow */

0 commit comments

Comments
 (0)