Skip to content

Conversation

@ivanjx
Copy link

@ivanjx ivanjx commented Aug 19, 2025

sometimes i need to add multiple torrents manually via the web gui into the same folder and it really slows me down typing/copy pasting the same full path every single time. with datalist it will help me a lot and avoid typos.

Notes: Added autocompletion to folder path input with datalist.

@killemov
Copy link

A drawback of using datalist is that you still need to type some matching characters. For Shift I have opted to create a select box from a curated set of paths with an additional ellipsis (…) option that changes the select box into a text input field with the last selected option as input.

@ckerr ckerr added type:feat A new feature needs code review needs notes PR title needs a one-sentence paragraph starting with 'Notes:' to summarize for the release notes labels Nov 3, 2025
@Rukario Rukario mentioned this pull request Nov 3, 2025
4 tasks
@ckerr
Copy link
Member

ckerr commented Nov 10, 2025

@ivanjx can you add a Notes: paragraph to the PR description for the release notes?

@transmission/contributors could someone review this? @killemov ?

@ckerr ckerr force-pushed the dest-folder-autocomplete branch from 8fd3bf7 to 4823bd3 Compare November 10, 2025 00:17
Copy link

@killemov killemov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My main problem with this solution is rebuilding the datalist on every focus(). Can you find a cached solution for this?

Comment on lines 193 to 195
let torrents = this.controller._rows
.toSorted((a, b) => b.getTorrent().getDateAdded() - a.getTorrent().getDateAdded())
.map(row => row.getTorrent());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. First map then sort.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe i will just not sort and just leave it as the original sorting being used.

Copy link

@killemov killemov Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

let torrents = this.controller._rows
   .map(row => row.getTorrent())
   .sort((a, b) => b.getDateAdded() - a.getDateAdded());

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if _rows are already being sorted by the controller then toSorted is unnecessary i think. see my other comment below.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in JavaScript, chaining .map() and .toSorted() does create two intermediate arrays — one after .map(), and another after .toSorted().

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured you needed the last added X torrents to get the last X used paths. Are you sure the controller has the row/torrents in the correct order already?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if they have the sorting set to Age then yes

@ivanjx
Copy link
Author

ivanjx commented Nov 10, 2025

regarding sorting i think it is better to keep the ordering in _rows because it should be already being sorted based on the gui.
regarding rebuilding on focus i think i will switch it to just rebuild on load. if we want caching then we need to track the list changes which is too much of work.

@ivanjx
Copy link
Author

ivanjx commented Nov 11, 2025

@ckerr should we sort by age again manually in the open-dialog or should we let it order based on _rows ordering?

Comment on lines +194 to +204
if (!dir || dirs.has(dir)) {
continue;
}
dirs.add(dir);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both Set.has and Set.add traverse the entire set to check if an item is in the collection. You should change Set to Array since you are checking the collection yourself anyway.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or just avoid the has() call?

if (dir)
  dirs.add(dir)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or just avoid the has() call?

if (dir)
  dirs.add(dir)

Unfortunately not. There has to be some check to skip the loop if the dir already exists to avoid duplicate option elements.

Copy link
Author

@ivanjx ivanjx Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

afaik Set.has is O(1) while Array.includes is O(n)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick search led me to this article: Using Array.prototype.includes() vs Set.prototype.has() to filter arrays

And the comments painted a different picture from the main article: It appears to be implementation dependent.

If you want to be sure you can do some online Javascript performance testing.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from the article's comments there's a sample benchmark code and this is what i got:

image

in our case perf may be not noticeable at all but i think it is still good practice to use Set.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Then we can simplify(?) to:

workarea.append((() => {
  const datalist = document.createElement('datalist');
  datalist.id = 'add-dialog-folder-datalist';

  var i = 0;
  for (const dir of new Set(this.controller._rows.map(row => row.getTorrent().getDownloadDir().trim()))) {
    const option = document.createElement('option');
    option.value = dir;
    datalist.append(option);

    if (++i >= max_datalist_options) { // do not explode the ui
      break;
    }
  }
  return datalist;
})());

IF an option is created with a null and/or "" it should not appear in the input because you have to type something.

Copy link
Contributor

@Rukario Rukario Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think by using map() we could very well be using reduce(), it is accumulator for new Set() and do less job when reaching 100

    const datalist = document.createElement('DATALIST');
    datalist.id = 'add-dialog-folder-datalist';
    const dirs = this.controller._rows.reduce(
      (accumulator, row) => {
        if (accumulator.size < max_datalist_options) {
          accumulator.add(row.getTorrent().getDownloadDir().trim());
        }
        return accumulator;
      },
      new Set(),
    );
    for (const dir of dirs) {
      datalist.append(new Option(dir));
    }

    workarea.append(input, datalist);

Performance untested

@ckerr
Copy link
Member

ckerr commented Nov 12, 2025

@ckerr should we sort by age again manually in the open-dialog or should we let it order based on _rows ordering?

I am not sure what I think yet -- lemme try test-driving this and see how it looks to me in practice

@ivanjx ivanjx force-pushed the dest-folder-autocomplete branch from 452d277 to 1699c71 Compare December 12, 2025 04:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs code review needs notes PR title needs a one-sentence paragraph starting with 'Notes:' to summarize for the release notes scope:web type:feat A new feature

Development

Successfully merging this pull request may close these issues.

5 participants