Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion app/controllers/profiles_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ def update
private

def user_params
params.expect(user: [:username, :password_challenge, :stories_order])
params.expect(
user: [
:username,
:password_challenge,
:stories_order,
:enclosure_filename_format
]
)
end
end
67 changes: 67 additions & 0 deletions app/javascript/controllers/enclosure_download_controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {Controller} from "@hotwired/stimulus";

function sanitizeFilename(name: string): string {
return name.replace(/[<>:"/\\|?*\x00-\x1F]/g, "_").trim();
}

function extractExtension(url: string): string {
try {
const pathname = new URL(url).pathname;
const match = pathname.match(/\.(\w+)$/);
return match ? `.${match[1]}` : "";
} catch {
return "";
}
}

const MAX_FILENAME_LENGTH = 200;

export default class extends Controller {
static values = {title: String, source: String, date: String, format: String};

declare titleValue: string;
declare sourceValue: string;
declare dateValue: string;
declare formatValue: string;

connect(): void {
this.element.addEventListener("click", this.handleClick);
}

disconnect(): void {
this.element.removeEventListener("click", this.handleClick);
}

handleClick = (event: Event): void => {
if (this.formatValue !== "date_source_title") return;

event.preventDefault();

const href = (this.element as HTMLAnchorElement).href;
const ext = extractExtension(href);
const basename = sanitizeFilename(
`${this.dateValue} - ${this.sourceValue} - ${this.titleValue}`,
);
const filename =
basename.slice(0, MAX_FILENAME_LENGTH - ext.length) + ext;

fetch(href)
.then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.blob();
})
.then((blob) => {
const objectUrl = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = objectUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(objectUrl);
})
.catch(() => {
window.open(href);
});
};
}
3 changes: 3 additions & 0 deletions app/javascript/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@ import {application} from "./application";
import DialogController from "./dialog_controller";
application.register("dialog", DialogController);

import EnclosureDownloadController from "./enclosure_download_controller";
application.register("enclosure-download", EnclosureDownloadController);

import HotkeysController from "./hotkeys_controller";
application.register("hotkeys", HotkeysController);
11 changes: 11 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,21 @@ class User < ApplicationRecord

enum :stories_order, { desc: "desc", asc: "asc" }, prefix: true

ENCLOSURE_FILENAME_FORMATS = ["original", "date_source_title"].freeze

store_accessor :settings, :enclosure_filename_format
validates :enclosure_filename_format,
inclusion: { in: ENCLOSURE_FILENAME_FORMATS },
allow_nil: true

attr_accessor :password_challenge

# `password_challenge` logic should be able to be removed in Rails 7.1
# https://blog.appsignal.com/2023/02/15/whats-new-in-rails-7-1.html#password-challenge-via-has_secure_password
def enclosure_filename_format
super.presence || "original"
end

def password_challenge_matches
return unless password_challenge

Expand Down
8 changes: 7 additions & 1 deletion app/views/js/templates/_story.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@
<h1>
<a href="{{= permalink }}">{{= title }}</a>
{{ if (enclosure_url) { }}
<a class="story-enclosure" target="_blank" href="{{= enclosure_url }}">
<a class="story-enclosure"
href="{{= enclosure_url }}"
data-controller="enclosure-download"
data-enclosure-download-title-value="{{= title }}"
data-enclosure-download-source-value="{{= source }}"
data-enclosure-download-date-value="{{= pretty_date }}"
data-enclosure-download-format-value="<%= current_user.enclosure_filename_format %>">
<i class="fa fa-download"></i>
</a>
{{ } }}
Expand Down
2 changes: 2 additions & 0 deletions app/views/profiles/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<legend><%= t(".stories_feed_settings") %></legend>
<%= form.label :stories_order %>
<%= form.select :stories_order, User.stories_orders.transform_keys {|k| User.human_attribute_name("stories_order.#{k}") } %>
<%= form.label :enclosure_filename_format %>
<%= form.select :enclosure_filename_format, User::ENCLOSURE_FILENAME_FORMATS.map {|f| [User.human_attribute_name("enclosure_filename_format.#{f}"), f] } %>
</fieldset>
<%= form.submit("Update") %>
<% end %>
Expand Down
8 changes: 7 additions & 1 deletion app/views/stories/_templates.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@
<h1>
<a href="{{= permalink }}">{{= title }}</a>
{{ if (enclosure_url) { }}
<a class="story-enclosure" target="_blank" href="{{= enclosure_url }}">
<a class="story-enclosure"
href="{{= enclosure_url }}"
data-controller="enclosure-download"
data-enclosure-download-title-value="{{= title }}"
data-enclosure-download-source-value="{{= source }}"
data-enclosure-download-date-value="{{= pretty_date }}"
data-enclosure-download-format-value="<%= current_user.enclosure_filename_format %>">
<i class="fa fa-download"></i>
</a>
{{ } }}
Expand Down
6 changes: 5 additions & 1 deletion config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ en:
attributes:
user:
stories_order: "Stories feed order"
enclosure_filename_format: "Download filename format"
user/stories_order:
asc: "Oldest first"
desc: "Newest first"
desc: "Newest first"
user/enclosure_filename_format:
original: "Original"
date_source_title: "Date - Source - Title"
7 changes: 7 additions & 0 deletions db/migrate/20260223045507_add_settings_to_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class AddSettingsToUsers < ActiveRecord::Migration[8.1]
def change
add_column :users, :settings, :jsonb, default: {}, null: false
end
end
127 changes: 64 additions & 63 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading