scriptorium 0.0.3 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.lt3 +324 -0
- data/README.md +3155 -1
- data/assets/.DS_Store +0 -0
- data/assets/README.md +44 -0
- data/assets/back-icon.png +0 -0
- data/assets/icons/facebook.svg +1 -0
- data/assets/icons/github.svg +1 -0
- data/assets/icons/instagram.svg +1 -0
- data/assets/icons/reddit.svg +1 -0
- data/assets/icons/ui/.DS_Store +0 -0
- data/assets/icons/ui/back.png +0 -0
- data/assets/icons/ui/copy.png +0 -0
- data/assets/icons/ui/down.png +0 -0
- data/assets/icons/ui/end.png +0 -0
- data/assets/icons/ui/exit.png +0 -0
- data/assets/icons/ui/foo +10 -0
- data/assets/icons/ui/home.png +0 -0
- data/assets/icons/ui/left.png +0 -0
- data/assets/icons/ui/next.png +0 -0
- data/assets/icons/ui/right.png +0 -0
- data/assets/icons/ui/start.png +0 -0
- data/assets/icons/ui/up.png +0 -0
- data/assets/icons/x.svg +1 -0
- data/assets/icons/youtube.svg +1 -0
- data/assets/samples/placeholder.svg +9 -0
- data/assets/themes/standard/favicon.svg +6 -0
- data/bin/scriptorium +1511 -0
- data/doc/README.txt +6 -0
- data/doc/anti-amnesia/20250727-054000-scriptorium-overview.md +95 -0
- data/doc/anti-amnesia/20250727-060000-api-design-tui-planning.md +34 -0
- data/doc/anti-amnesia/20250727-061000-runeblog-tui-analysis.md +50 -0
- data/doc/anti-amnesia/20250727-123000-anti-amnesia-conventions.md +31 -0
- data/doc/anti-amnesia/20250727-154000-livetext-plugin-file-stats.md +73 -0
- data/doc/anti-amnesia/20250727-172600-cursor-rbenv-ruby-version-mystery.md +64 -0
- data/doc/anti-amnesia/20250727-172600-unified-minitest-framework.md +70 -0
- data/doc/anti-amnesia/20250727-172900-ai-cognitive-assessment-capabilities.md +40 -0
- data/doc/anti-amnesia/20250727-173000-widget-testing-achievement.md +110 -0
- data/doc/anti-amnesia/20250727-180000-post-id-num-refactoring.md +73 -0
- data/doc/anti-amnesia/20250728-124243-aaa-syntax-clarification.md +46 -0
- data/doc/anti-amnesia/20250728-124421-conversation-summary-concise.md +124 -0
- data/doc/anti-amnesia/20250729-190000-scriptorium-tui-testing-complete.md +46 -0
- data/doc/anti-amnesia/20250729-200000-scriptorium-tui-testing-edit-file-workflow.md +97 -0
- data/doc/anti-amnesia/20250729-210000-reddit-autopost-integration-complete.md +158 -0
- data/doc/anti-amnesia/20250729-211500-dependency-management-system.md +211 -0
- data/doc/anti-amnesia/20250729-213000-python-virtual-environment-setup.md +141 -0
- data/doc/anti-amnesia/20250729-214500-theme-management-commands.md +211 -0
- data/doc/anti-amnesia/20250729-215000-version-update-to-0.6.0.md +134 -0
- data/doc/anti-amnesia/20250729-220000-user-guide-complete.md +41 -0
- data/doc/anti-amnesia/20250804-190500-cognitive-loop-bug.md +45 -0
- data/doc/anti-amnesia/20250804-190700-anti-amnesia-timestamping-fix.md +30 -0
- data/doc/anti-amnesia/20250804-213700-publishing-test-fix.md +49 -0
- data/doc/anti-amnesia/20250804-214400-additional-test-fixes.md +46 -0
- data/doc/anti-amnesia/20250804-220000-asset-function-logic-clarification.md +41 -0
- data/doc/anti-amnesia/20250806-202032-asset-function-logic-clarification.md +41 -0
- data/doc/anti-amnesia/20250807-213025.md +116 -0
- data/doc/anti-amnesia/20250813-082428-syntax-highlighting-and-navigation-improvements.md +256 -0
- data/doc/banner_svg_config.md +114 -0
- data/doc/contrib.lt3 +8 -0
- data/doc/dependencies.md +281 -0
- data/doc/hacker.lt3 +5 -0
- data/doc/reddit_credentials_template.json +8 -0
- data/doc/reddit_integration.md +207 -0
- data/doc/user.lt3 +38 -0
- data/doc/user_guide_section_1.md +137 -0
- data/doc/user_guide_section_10.md +515 -0
- data/doc/user_guide_section_11.md +708 -0
- data/doc/user_guide_section_2.md +233 -0
- data/doc/user_guide_section_3.md +5 -0
- data/doc/user_guide_section_4.md +221 -0
- data/doc/user_guide_section_5.md +243 -0
- data/doc/user_guide_section_6.md +147 -0
- data/doc/user_guide_section_7.md +311 -0
- data/doc/user_guide_section_8.md +224 -0
- data/doc/user_guide_section_9.md +375 -0
- data/doc/userdoc-toc.txt +88 -0
- data/lib/rouge/lexers/livetext.rb +74 -0
- data/lib/scriptorium/api.rb +640 -0
- data/lib/scriptorium/banner_svg.rb +742 -0
- data/lib/scriptorium/contract.rb +33 -0
- data/lib/scriptorium/exceptions.rb +170 -1
- data/lib/scriptorium/helpers.rb +475 -0
- data/lib/scriptorium/post.rb +195 -0
- data/lib/scriptorium/reddit.rb +83 -0
- data/lib/scriptorium/repo.rb +624 -0
- data/lib/scriptorium/standard_files.rb +515 -0
- data/lib/scriptorium/syntax_highlighter.rb +234 -0
- data/lib/scriptorium/theme.rb +179 -0
- data/lib/scriptorium/version.rb +2 -2
- data/lib/scriptorium/view.rb +976 -0
- data/lib/scriptorium/widgets/featured_posts.rb +149 -0
- data/lib/scriptorium/widgets/links.rb +112 -0
- data/lib/scriptorium/widgets/pages.rb +133 -0
- data/lib/scriptorium/widgets/widget.rb +133 -0
- data/lib/scriptorium.rb +21 -40
- data/lib/skeleton.rb +8 -2
- data/scriptorium.gemspec +15 -4
- data/test/README.md +69 -0
- data/test/all +43 -0
- data/test/api_demo.rb +99 -0
- data/test/assets/imagenotfound.jpg +0 -0
- data/test/assets/images/.DS_Store +0 -0
- data/test/assets/images/README.md +27 -0
- data/test/assets/images/odd_aspect.png +0 -0
- data/test/assets/images/perfect.png +0 -0
- data/test/assets/images/small.png +0 -0
- data/test/assets/images/tall.png +0 -0
- data/test/assets/images/very_tall.png +0 -0
- data/test/assets/images/very_wide.png +0 -0
- data/test/assets/images/wide.png +0 -0
- data/test/assets/testbanner.jpg +0 -0
- data/test/banner_svg/simple_helpers.rb +13 -0
- data/test/banner_svg/unit.rb +768 -0
- data/test/ed_test.rb +204 -0
- data/test/integration/cursor_banner_combinations.rb +193 -0
- data/test/integration/cursor_banner_features.rb +374 -0
- data/test/integration/integration_test.rb +326 -0
- data/test/livetext_plugin_test.rb +229 -0
- data/test/manual/asset_mgmt.rb +67 -0
- data/test/manual/banner-tests/config.txt +3 -0
- data/test/manual/banner-tests/index.html +45 -0
- data/test/manual/banner-tests/test01.html +58 -0
- data/test/manual/banner-tests/test02.html +58 -0
- data/test/manual/banner-tests/test03.html +58 -0
- data/test/manual/banner-tests/test04.html +65 -0
- data/test/manual/banner-tests/test05.html +65 -0
- data/test/manual/banner-tests/test06.html +65 -0
- data/test/manual/banner-tests/test07.html +65 -0
- data/test/manual/banner-tests/test08.html +59 -0
- data/test/manual/banner-tests/test09.html +59 -0
- data/test/manual/banner-tests/test10.html +59 -0
- data/test/manual/banner-tests/test11.html +59 -0
- data/test/manual/banner-tests/test12.html +59 -0
- data/test/manual/banner-tests/test13.html +59 -0
- data/test/manual/banner-tests/test14.html +59 -0
- data/test/manual/banner-tests/test15.html +58 -0
- data/test/manual/banner-tests/test16.html +58 -0
- data/test/manual/banner-tests/test17.html +58 -0
- data/test/manual/banner-tests/test18.html +68 -0
- data/test/manual/banner-tests/test19.html +68 -0
- data/test/manual/banner-tests/test20.html +68 -0
- data/test/manual/banner-tests/test21.html +68 -0
- data/test/manual/banner-tests/test22.html +68 -0
- data/test/manual/banner-tests/test23.html +68 -0
- data/test/manual/banner-tests/test24.html +68 -0
- data/test/manual/banner-tests/test25.html +67 -0
- data/test/manual/banner_environment.rb +192 -0
- data/test/manual/deploy_symlink_demo.rb +142 -0
- data/test/manual/environment.rb +67 -0
- data/test/manual/make_banner.rb +153 -0
- data/test/manual/sample_banner_config.txt +12 -0
- data/test/manual/symlink_demo.rb +117 -0
- data/test/manual/test1.rb +47 -0
- data/test/manual/test2.rb +12 -0
- data/test/manual/test3.rb +38 -0
- data/test/manual/test4.rb +40 -0
- data/test/manual/test5.rb +24 -0
- data/test/manual/test6.rb +73 -0
- data/test/manual/test_banner_combinations.rb +120 -0
- data/test/manual/test_banner_features.rb +306 -0
- data/test/manual/test_banner_from_file.rb +150 -0
- data/test/manual/test_banner_in_header.rb +35 -0
- data/test/manual/test_code_highlighting.rb +68 -0
- data/test/manual/test_complex_header.rb +74 -0
- data/test/manual/test_empty_header.rb +32 -0
- data/test/manual/test_radial_custom.rb +58 -0
- data/test/manual/test_radial_large_radius.rb +52 -0
- data/test/manual/test_svg_debug.rb +47 -0
- data/test/manual/test_syntax_highlighting.rb +147 -0
- data/test/pages-demo/config/currentview.txt +1 -0
- data/test/pages-demo/views/demo/config/bootstrap_css.txt +5 -0
- data/test/pages-demo/views/demo/config/bootstrap_js.txt +4 -0
- data/test/pages-demo/views/demo/config/common.js +57 -0
- data/test/pages-demo/views/demo/config/footer.txt +1 -0
- data/test/pages-demo/views/demo/config/global-head.txt +8 -0
- data/test/pages-demo/views/demo/config/header.txt +1 -0
- data/test/pages-demo/views/demo/config/layout.txt +1 -0
- data/test/pages-demo/views/demo/config/left.txt +1 -0
- data/test/pages-demo/views/demo/config/main.txt +1 -0
- data/test/pages-demo/views/demo/config/right.txt +1 -0
- data/test/pages-demo/views/demo/config.txt +3 -0
- data/test/pages-demo/views/demo/output/panes/footer.html +1 -0
- data/test/pages-demo/views/demo/output/panes/header.html +1 -0
- data/test/pages-demo/views/demo/output/panes/left.html +1 -0
- data/test/pages-demo/views/demo/output/panes/main.html +1 -0
- data/test/pages-demo/views/demo/output/panes/right.html +1 -0
- data/test/rubytext/rubytext_comprehensive_test.rb +307 -0
- data/test/rubytext/rubytext_demo_test.rb +42 -0
- data/test/rubytext/rubytext_testing_guide.md +277 -0
- data/test/run_automated_tests.rb +45 -0
- data/test/scriptorium-TEST-1754622690-146/config/bootstrap_css.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/config/bootstrap_js.txt +4 -0
- data/test/scriptorium-TEST-1754622690-146/config/common.js +57 -0
- data/test/scriptorium-TEST-1754622690-146/config/currentview.txt +1 -0
- data/test/scriptorium-TEST-1754622690-146/config/global-head.txt +9 -0
- data/test/scriptorium-TEST-1754622690-146/config/last_post_num.txt +1 -0
- data/test/scriptorium-TEST-1754622690-146/config/os_helpers.rb +4 -0
- data/test/scriptorium-TEST-1754622690-146/config/widgets.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/posts/0001/meta.txt +8 -0
- data/test/scriptorium-TEST-1754622690-146/posts/0001/source.lt3 +6 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/README.txt +1 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/config.txt +1 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/initial/post.lt3 +12 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/footer.txt +2 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/header.txt +4 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/left.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/main.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/right.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/gen/text.css +1 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/layout.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index.lt3 +1 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index_entry.lt3 +14 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/post.lt3 +13 -0
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/widget.lt3 +1 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_css.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_js.txt +4 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/common.js +57 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/deploy.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/footer.txt +2 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/global-head.txt +9 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/header.txt +4 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/layout.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/left.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/main.txt +5 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/reddit.txt +10 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/right.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/social.txt +7 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/status.txt +7 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/config.txt +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/footer.html +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/header.html +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/left.html +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/main.html +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/right.html +3 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/footer.html +1 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/header.html +1 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/left.html +1 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/main.html +1 -0
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/right.html +1 -0
- data/test/staging/.DS_Store +0 -0
- data/test/syntax_highlighting_test.lt3 +124 -0
- data/test/test_helpers.rb +230 -0
- data/test/tui_editor_integration_test.rb +296 -0
- data/test/tui_integration_test.rb +637 -0
- data/test/unit/api.rb +1056 -0
- data/test/unit/asset_management.rb +245 -0
- data/test/unit/clipboard_test.rb +60 -0
- data/test/unit/contract_test.rb +91 -0
- data/test/unit/core.rb +857 -0
- data/test/unit/deploy_test.rb +187 -0
- data/test/unit/gem_asset_management.rb +189 -0
- data/test/unit/livetext_basic.rb +69 -0
- data/test/unit/livetext_compatibility.rb +89 -0
- data/test/unit/post.rb +244 -0
- data/test/unit/read_commented_file_test.rb +276 -0
- data/test/unit/reddit_test.rb +235 -0
- data/test/unit/repo.rb +548 -0
- data/test/unit/social_test.rb +369 -0
- data/test/unit/symlink_test.rb +213 -0
- data/test/unit/view.rb +431 -0
- data/test/unit/widgets.rb +669 -0
- data/test/wizard_test.rb +123 -0
- data/ui/README.md +67 -0
- data/ui/common/lib/ui_common.rb +8 -0
- data/ui/rubytext/README.md +191 -0
- data/ui/rubytext/bin/scriptorium-rubytext +402 -0
- data/ui/rubytext/lib/rubytext_ui.rb +300 -0
- data/ui/tui/bin/scriptorium +1420 -0
- data/ui/tui/test/tui_test.rb +23 -0
- data/ui/web/app/app.rb +1378 -0
- data/ui/web/app/error_helpers.rb +150 -0
- data/ui/web/app/views/advanced_config.erb +190 -0
- data/ui/web/app/views/asset_management.erb +589 -0
- data/ui/web/app/views/banner_config.erb +200 -0
- data/ui/web/app/views/configure_view.erb +401 -0
- data/ui/web/app/views/dashboard.erb +162 -0
- data/ui/web/app/views/deploy_config.erb +146 -0
- data/ui/web/app/views/edit_pages.erb +195 -0
- data/ui/web/app/views/edit_post.erb +54 -0
- data/ui/web/app/views/error_page.erb +29 -0
- data/ui/web/app/views/header_config.erb +155 -0
- data/ui/web/app/views/layout_config.erb +147 -0
- data/ui/web/app/views/navbar_config.erb +411 -0
- data/ui/web/app/views/view_dashboard.erb +138 -0
- data/ui/web/bin/scriptorium-web +153 -0
- data/ui/web/test/web_basic_test.rb +38 -0
- data/ui/web/test_navbar.txt +7 -0
- data/ui/web/tmp/web_server.log +5 -0
- data/ui/web/tmp/web_server.pid +1 -0
- metadata +359 -7
- data/lib/scriptorium/engine.rb +0 -22
- data/test/engine/unit.rb +0 -44
data/ui/web/app/app.rb
ADDED
@@ -0,0 +1,1378 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'sinatra'
|
4
|
+
require 'sinatra/reloader' if development?
|
5
|
+
require 'fileutils'
|
6
|
+
require 'pathname'
|
7
|
+
begin
|
8
|
+
require 'fastimage'
|
9
|
+
rescue LoadError
|
10
|
+
# FastImage not available, will handle gracefully
|
11
|
+
end
|
12
|
+
require_relative '../../../lib/scriptorium'
|
13
|
+
require_relative 'error_helpers'
|
14
|
+
|
15
|
+
include ErrorHelpers
|
16
|
+
|
17
|
+
class ScriptoriumWeb < Sinatra::Base
|
18
|
+
set :port, 4567
|
19
|
+
set :bind, '0.0.0.0'
|
20
|
+
set :views, File.join(__dir__, 'views')
|
21
|
+
set :show_exceptions, false # Disable Sinatra's default error display
|
22
|
+
|
23
|
+
# Enable reloading in development
|
24
|
+
configure :development do
|
25
|
+
register Sinatra::Reloader
|
26
|
+
end
|
27
|
+
|
28
|
+
# Global error handler
|
29
|
+
error do
|
30
|
+
error_info = friendly_error_message(env['sinatra.error'])
|
31
|
+
@error = error_info[:message]
|
32
|
+
@suggestion = error_info[:suggestion]
|
33
|
+
erb :error_page
|
34
|
+
end
|
35
|
+
|
36
|
+
# Initialize API instance
|
37
|
+
before do
|
38
|
+
begin
|
39
|
+
@api = Scriptorium::API.new
|
40
|
+
# Use absolute path to the test repository
|
41
|
+
test_repo_path = File.join(__dir__, "..", "scriptorium-TEST")
|
42
|
+
@api.open_repo(test_repo_path) if Dir.exist?(test_repo_path)
|
43
|
+
rescue => e
|
44
|
+
@api = nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Main dashboard
|
49
|
+
get '/' do
|
50
|
+
@current_view = @api&.current_view
|
51
|
+
@views = @api&.views || []
|
52
|
+
begin
|
53
|
+
if @api&.instance_variable_get(:@repo)
|
54
|
+
|
55
|
+
# Only try to load posts if we have a current view
|
56
|
+
if @current_view
|
57
|
+
@posts = @api.posts(@current_view.name) || []
|
58
|
+
if @posts.length > 0
|
59
|
+
end
|
60
|
+
else
|
61
|
+
@posts = []
|
62
|
+
end
|
63
|
+
else
|
64
|
+
@posts = []
|
65
|
+
end
|
66
|
+
rescue => e
|
67
|
+
@posts = []
|
68
|
+
end
|
69
|
+
@error = @error || params[:error]
|
70
|
+
@message = params[:message]
|
71
|
+
|
72
|
+
erb :dashboard
|
73
|
+
end
|
74
|
+
|
75
|
+
# Change view
|
76
|
+
post '/change_view' do
|
77
|
+
view_name = params[:view_name]
|
78
|
+
|
79
|
+
if view_name.nil? || view_name.strip.empty?
|
80
|
+
redirect "/?error=No view selected"
|
81
|
+
return
|
82
|
+
end
|
83
|
+
|
84
|
+
begin
|
85
|
+
view = @api.lookup_view(view_name)
|
86
|
+
@api.view(view_name)
|
87
|
+
redirect '/?message=View changed successfully'
|
88
|
+
rescue => e
|
89
|
+
redirect "/?error=Failed to change view: #{e.message}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Create new repository
|
94
|
+
post '/create_repo' do
|
95
|
+
begin
|
96
|
+
@api.create_repo("scriptorium-TEST")
|
97
|
+
# After creating, open the repo
|
98
|
+
@api.open_repo("scriptorium-TEST")
|
99
|
+
redirect '/?message=Repository created successfully'
|
100
|
+
rescue => e
|
101
|
+
redirect "/?error=Failed to create repository: #{e.message}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Create new view
|
106
|
+
post '/create_view' do
|
107
|
+
begin
|
108
|
+
validate_required_params(params, :name, :title)
|
109
|
+
|
110
|
+
name = params[:name].strip
|
111
|
+
title = params[:title].strip
|
112
|
+
subtitle = params[:subtitle]&.strip || ""
|
113
|
+
|
114
|
+
@api.create_view(name, title, subtitle, theme: "standard")
|
115
|
+
redirect "/?message=View '#{name}' created successfully"
|
116
|
+
rescue => e
|
117
|
+
error_info = friendly_error_message(e)
|
118
|
+
redirect "/?error=#{error_info[:message]}&suggestion=#{error_info[:suggestion]}"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Create new post
|
123
|
+
post '/create_post' do
|
124
|
+
begin
|
125
|
+
validate_required_params(params, :title)
|
126
|
+
|
127
|
+
current_view = @api&.current_view
|
128
|
+
if current_view.nil?
|
129
|
+
redirect "/?error=No view selected. Please select a view first."
|
130
|
+
return
|
131
|
+
end
|
132
|
+
|
133
|
+
# Create a draft first
|
134
|
+
draft_path = @api.create_draft(
|
135
|
+
title: params[:title].strip,
|
136
|
+
body: "", # Empty body to start
|
137
|
+
views: current_view.name,
|
138
|
+
tags: nil,
|
139
|
+
blurb: nil
|
140
|
+
)
|
141
|
+
|
142
|
+
# Convert draft to post immediately
|
143
|
+
post_num = @api.finish_draft(draft_path)
|
144
|
+
# Generate the post to create meta.txt and other files
|
145
|
+
begin
|
146
|
+
STDERR.puts "DEBUG: About to call generate_post for post #{post_num}"
|
147
|
+
STDERR.puts "DEBUG: Current working directory: #{Dir.pwd}"
|
148
|
+
STDERR.puts "DEBUG: API root: #{@api.root}"
|
149
|
+
@api.generate_post(post_num)
|
150
|
+
STDERR.puts "DEBUG: generate_post completed successfully"
|
151
|
+
# Check if meta.txt was created
|
152
|
+
meta_file = @api.root/"posts"/"#{post_num.to_s.rjust(4, '0')}"/"meta.txt"
|
153
|
+
STDERR.puts "DEBUG: Meta file path: #{meta_file}"
|
154
|
+
STDERR.puts "DEBUG: Meta file exists: #{File.exist?(meta_file)}"
|
155
|
+
redirect "/?message=Post '#{params[:title].strip}' created successfully (##{post_num})"
|
156
|
+
rescue => e
|
157
|
+
# Log the actual error for debugging
|
158
|
+
STDERR.puts "ERROR in generate_post: #{e.class}: #{e.message}"
|
159
|
+
STDERR.puts e.backtrace.join("\n")
|
160
|
+
error_info = friendly_error_message(e)
|
161
|
+
redirect "/?error=#{error_info[:message]}&suggestion=#{error_info[:suggestion]}"
|
162
|
+
end
|
163
|
+
rescue => e
|
164
|
+
error_info = friendly_error_message(e)
|
165
|
+
redirect "/?error=#{error_info[:message]}&suggestion=#{error_info[:suggestion]}"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Edit post (redirects to file editing)
|
170
|
+
post '/edit_post' do
|
171
|
+
begin
|
172
|
+
validate_required_params(params, :post_id)
|
173
|
+
|
174
|
+
unless validate_post_id(params[:post_id])
|
175
|
+
redirect "/?error=Invalid post ID&suggestion=Please provide a valid post number."
|
176
|
+
return
|
177
|
+
end
|
178
|
+
|
179
|
+
post = @api.post(params[:post_id].to_i)
|
180
|
+
if post.nil?
|
181
|
+
redirect "/?error=Post not found&suggestion=The post may have been deleted or moved."
|
182
|
+
return
|
183
|
+
end
|
184
|
+
|
185
|
+
# Redirect to the edit page
|
186
|
+
redirect "/edit_post/#{params[:post_id]}"
|
187
|
+
rescue => e
|
188
|
+
error_info = friendly_error_message(e)
|
189
|
+
redirect "/?error=#{error_info[:message]}&suggestion=#{error_info[:suggestion]}"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Show edit post page
|
194
|
+
get '/edit_post/:id' do
|
195
|
+
post_id = params[:id]&.to_i
|
196
|
+
|
197
|
+
if post_id.nil? || post_id <= 0
|
198
|
+
redirect "/?error=Invalid post ID"
|
199
|
+
return
|
200
|
+
end
|
201
|
+
|
202
|
+
begin
|
203
|
+
@post = @api.post(post_id)
|
204
|
+
if @post.nil?
|
205
|
+
redirect "/?error=Post not found"
|
206
|
+
return
|
207
|
+
end
|
208
|
+
|
209
|
+
# Read the source file content
|
210
|
+
source_file = @api.root/"posts"/@post.num/"source.lt3"
|
211
|
+
if File.exist?(source_file)
|
212
|
+
@content = File.read(source_file)
|
213
|
+
else
|
214
|
+
@content = "# #{@post.title}\n\n"
|
215
|
+
end
|
216
|
+
|
217
|
+
erb :edit_post
|
218
|
+
rescue => e
|
219
|
+
redirect "/?error=Failed to load post: #{e.message}"
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# Save edited post
|
224
|
+
post '/save_post/:id' do
|
225
|
+
post_id = params[:id]&.to_i
|
226
|
+
content = params[:content]
|
227
|
+
|
228
|
+
if post_id.nil? || post_id <= 0
|
229
|
+
redirect "/?error=Invalid post ID"
|
230
|
+
return
|
231
|
+
end
|
232
|
+
|
233
|
+
if content.nil?
|
234
|
+
redirect "/edit_post/#{post_id}?error=No content provided"
|
235
|
+
return
|
236
|
+
end
|
237
|
+
|
238
|
+
begin
|
239
|
+
post = @api.post(post_id)
|
240
|
+
if post.nil?
|
241
|
+
redirect "/?error=Post not found"
|
242
|
+
return
|
243
|
+
end
|
244
|
+
|
245
|
+
# Write the content to the source file
|
246
|
+
source_file = @api.root/"posts"/post.num/"source.lt3"
|
247
|
+
File.write(source_file, content)
|
248
|
+
|
249
|
+
# Generate the post after saving
|
250
|
+
@api.generate_post(post_id)
|
251
|
+
|
252
|
+
redirect "/?message=Post ##{post_id} saved and generated successfully"
|
253
|
+
rescue => e
|
254
|
+
redirect "/edit_post/#{post_id}?error=Failed to save post: #{e.message}"
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# Generate post
|
259
|
+
post '/generate_post' do
|
260
|
+
post_id = params[:post_id]&.to_i
|
261
|
+
|
262
|
+
if post_id.nil? || post_id <= 0
|
263
|
+
redirect "/?error=Invalid post ID"
|
264
|
+
return
|
265
|
+
end
|
266
|
+
|
267
|
+
begin
|
268
|
+
post = @api.post(post_id)
|
269
|
+
if post.nil?
|
270
|
+
redirect "/?error=Post not found"
|
271
|
+
return
|
272
|
+
end
|
273
|
+
|
274
|
+
# Generate the post
|
275
|
+
@api.generate_post(post_id)
|
276
|
+
redirect "/?message=Post ##{post_id} generated successfully"
|
277
|
+
rescue => e
|
278
|
+
redirect "/?error=Failed to generate post: #{e.message}"
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
# Generate view
|
283
|
+
post '/generate_view' do
|
284
|
+
view_name = params[:view_name]
|
285
|
+
|
286
|
+
begin
|
287
|
+
if view_name.nil? || view_name.strip.empty?
|
288
|
+
redirect "/?error=No view specified"
|
289
|
+
return
|
290
|
+
end
|
291
|
+
|
292
|
+
# Generate the view
|
293
|
+
@api.generate_view(view_name)
|
294
|
+
redirect "/?message=View '#{view_name}' generated successfully"
|
295
|
+
rescue => e
|
296
|
+
redirect "/?error=Failed to generate view: #{e.message}"
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
# Preview view
|
301
|
+
post '/preview_view' do
|
302
|
+
view_name = params[:view_name]
|
303
|
+
|
304
|
+
begin
|
305
|
+
if view_name.nil? || view_name.strip.empty?
|
306
|
+
redirect "/?error=No view specified"
|
307
|
+
return
|
308
|
+
end
|
309
|
+
|
310
|
+
# Generate the view first to ensure it's up to date
|
311
|
+
@api.generate_view(view_name)
|
312
|
+
|
313
|
+
# Redirect to the generated index.html file
|
314
|
+
view_dir = @api.root/"views"/view_name
|
315
|
+
index_file = view_dir/"output"/"index.html"
|
316
|
+
|
317
|
+
if File.exist?(index_file)
|
318
|
+
# Return the HTML content directly for preview
|
319
|
+
content_type :html
|
320
|
+
File.read(index_file)
|
321
|
+
else
|
322
|
+
redirect "/?error=Preview file not found - view may not have been generated properly"
|
323
|
+
end
|
324
|
+
rescue => e
|
325
|
+
redirect "/?error=Failed to preview view: #{e.message}"
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
# Serve post files for preview
|
330
|
+
get '/preview/:view_name/posts/:filename' do
|
331
|
+
view_name = params[:view_name]
|
332
|
+
filename = params[:filename]
|
333
|
+
|
334
|
+
STDERR.puts "DEBUG: Preview request - view_name: #{view_name}, filename: #{filename}"
|
335
|
+
|
336
|
+
begin
|
337
|
+
if view_name.nil? || view_name.strip.empty? || filename.nil? || filename.strip.empty?
|
338
|
+
STDERR.puts "DEBUG: Missing parameters"
|
339
|
+
status 404
|
340
|
+
return "File not found"
|
341
|
+
end
|
342
|
+
|
343
|
+
# Construct the file path
|
344
|
+
post_file = @api.root/"views"/view_name/"output"/"posts"/filename
|
345
|
+
STDERR.puts "DEBUG: Looking for file: #{post_file}"
|
346
|
+
STDERR.puts "DEBUG: File exists: #{File.exist?(post_file)}"
|
347
|
+
|
348
|
+
if File.exist?(post_file)
|
349
|
+
content_type :html
|
350
|
+
File.read(post_file)
|
351
|
+
else
|
352
|
+
STDERR.puts "DEBUG: File not found"
|
353
|
+
status 404
|
354
|
+
"File not found: #{filename}"
|
355
|
+
end
|
356
|
+
rescue => e
|
357
|
+
STDERR.puts "DEBUG: Error: #{e.message}"
|
358
|
+
status 500
|
359
|
+
"Error loading file: #{e.message}"
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
# Show view configuration page
|
364
|
+
get '/configure_view/:name' do
|
365
|
+
begin
|
366
|
+
validate_required_params(params, :name)
|
367
|
+
|
368
|
+
unless validate_view_name(params[:name])
|
369
|
+
redirect "/?error=Invalid view name&suggestion=View names must contain only letters, numbers, hyphens, and underscores."
|
370
|
+
return
|
371
|
+
end
|
372
|
+
|
373
|
+
view = @api.lookup_view(params[:name])
|
374
|
+
if view.nil?
|
375
|
+
redirect "/?error=View not found&suggestion=The view '#{params[:name]}' does not exist. Check the view name or create it first."
|
376
|
+
return
|
377
|
+
end
|
378
|
+
|
379
|
+
@view = view
|
380
|
+
|
381
|
+
# Load view configuration safely
|
382
|
+
config_file = @api.root/"views"/params[:name]/"config.txt"
|
383
|
+
@config_content = safe_read_file(config_file, "# View configuration for #{params[:name]}\n")
|
384
|
+
|
385
|
+
# Load layout file safely
|
386
|
+
layout_file = @api.root/"views"/params[:name]/"config"/"layout.txt"
|
387
|
+
@layout_content = safe_read_file(layout_file, "# Layout configuration for #{params[:name]}\n")
|
388
|
+
|
389
|
+
erb :configure_view
|
390
|
+
rescue => e
|
391
|
+
error_info = friendly_error_message(e)
|
392
|
+
redirect "/?error=#{error_info[:message]}&suggestion=#{error_info[:suggestion]}"
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
# Save view configuration
|
397
|
+
post '/save_view_config/:name' do
|
398
|
+
view_name = params[:name]
|
399
|
+
|
400
|
+
begin
|
401
|
+
view = @api.lookup_view(view_name)
|
402
|
+
if view.nil?
|
403
|
+
redirect "/?error=View not found"
|
404
|
+
return
|
405
|
+
end
|
406
|
+
|
407
|
+
# Step 1: Save basic view information
|
408
|
+
if params[:view_title] && params[:view_subtitle] && params[:view_theme]
|
409
|
+
config_content = "title #{params[:view_title]}\n"
|
410
|
+
config_content += "subtitle #{params[:view_subtitle]}\n"
|
411
|
+
config_content += "theme #{params[:view_theme]}\n"
|
412
|
+
|
413
|
+
config_file = @api.root/"views"/view_name/"config.txt"
|
414
|
+
File.write(config_file, config_content)
|
415
|
+
end
|
416
|
+
|
417
|
+
# Step 2: Save layout configuration
|
418
|
+
if params[:containers]
|
419
|
+
layout_content = ""
|
420
|
+
containers = Array(params[:containers])
|
421
|
+
|
422
|
+
containers.each do |container|
|
423
|
+
case container
|
424
|
+
when 'header'
|
425
|
+
layout_content += "header # Top (banner? title? navbar? etc.)\n"
|
426
|
+
when 'left'
|
427
|
+
width = params[:left_width] || "15%"
|
428
|
+
layout_content += "left #{width} # Left sidebar, #{width} width\n"
|
429
|
+
when 'main'
|
430
|
+
layout_content += "main # Main (center) container - posts/etc.\n"
|
431
|
+
when 'right'
|
432
|
+
width = params[:right_width] || "15%"
|
433
|
+
layout_content += "right #{width} # Right sidebar, #{width} width\n"
|
434
|
+
when 'footer'
|
435
|
+
layout_content += "footer # Footer (copyright? mail? social media? etc.)\n"
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
layout_file = @api.root/"views"/view_name/"config"/"layout.txt"
|
440
|
+
FileUtils.mkdir_p(File.dirname(layout_file))
|
441
|
+
File.write(layout_file, layout_content)
|
442
|
+
end
|
443
|
+
|
444
|
+
# Step 3: Save container content files
|
445
|
+
containers = Array(params[:containers])
|
446
|
+
|
447
|
+
containers.each do |container|
|
448
|
+
content_param = "#{container}_content"
|
449
|
+
if params[content_param]
|
450
|
+
content_file = @api.root/"views"/view_name/"config"/"#{container}.txt"
|
451
|
+
FileUtils.mkdir_p(File.dirname(content_file))
|
452
|
+
File.write(content_file, params[content_param])
|
453
|
+
|
454
|
+
# If this is header with "banner svg", create default svg.txt
|
455
|
+
if container == 'header' && params[content_param].strip == 'banner svg'
|
456
|
+
svg_file = @api.root/"views"/view_name/"config"/"svg.txt"
|
457
|
+
unless File.exist?(svg_file)
|
458
|
+
# Create default SVG configuration
|
459
|
+
default_svg_content = "# SVG Banner Configuration\n"
|
460
|
+
default_svg_content += "# Light gradient background with dark text\n"
|
461
|
+
default_svg_content += "back.linear #f8f9fa #e9ecef lr\n"
|
462
|
+
default_svg_content += "text.color #374151\n"
|
463
|
+
default_svg_content += "title.style bold\n"
|
464
|
+
File.write(svg_file, default_svg_content)
|
465
|
+
end
|
466
|
+
end
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
redirect "/?message=View '#{view_name}' configuration saved successfully"
|
471
|
+
rescue => e
|
472
|
+
redirect "/configure_view/#{view_name}?error=Failed to save configuration: #{e.message}"
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
# Banner configuration page
|
477
|
+
get '/banner_config' do
|
478
|
+
@current_view = @api&.current_view
|
479
|
+
if @current_view.nil?
|
480
|
+
redirect "/?error=No view selected. Please select a view first."
|
481
|
+
return
|
482
|
+
end
|
483
|
+
|
484
|
+
# Get current SVG config
|
485
|
+
svg_file = @api.root/"views"/@current_view.name/"config"/"svg.txt"
|
486
|
+
@svg_config = File.exist?(svg_file) ? File.read(svg_file) : ""
|
487
|
+
|
488
|
+
# Generate current banner for display
|
489
|
+
begin
|
490
|
+
banner = Scriptorium::BannerSVG.new(@current_view.title, @current_view.subtitle)
|
491
|
+
|
492
|
+
# Use the same approach as View class
|
493
|
+
if @svg_config.strip.length > 0
|
494
|
+
svg_file = @api.root/"views"/@current_view.name/"config"/"svg.txt"
|
495
|
+
banner.parse_header_svg(svg_file)
|
496
|
+
else
|
497
|
+
# No config, use defaults
|
498
|
+
banner.parse_header_svg
|
499
|
+
end
|
500
|
+
|
501
|
+
@banner_svg = banner.generate_svg
|
502
|
+
rescue => e
|
503
|
+
@banner_svg = "<p>Error generating banner: #{e.message}</p>"
|
504
|
+
end
|
505
|
+
|
506
|
+
erb :banner_config
|
507
|
+
end
|
508
|
+
|
509
|
+
# Update banner configuration
|
510
|
+
post '/banner_config' do
|
511
|
+
@current_view = @api&.current_view
|
512
|
+
if @current_view.nil?
|
513
|
+
redirect "/?error=No view selected. Please select a view first."
|
514
|
+
return
|
515
|
+
end
|
516
|
+
|
517
|
+
begin
|
518
|
+
svg_config = params[:svg_config] || ""
|
519
|
+
|
520
|
+
# Save the SVG configuration
|
521
|
+
svg_file = @api.root/"views"/@current_view.name/"config"/"svg.txt"
|
522
|
+
FileUtils.mkdir_p(File.dirname(svg_file))
|
523
|
+
File.write(svg_file, svg_config)
|
524
|
+
|
525
|
+
# Update status
|
526
|
+
update_config_status(@current_view.name, "banner", true)
|
527
|
+
|
528
|
+
redirect "/banner_config?message=Banner configuration updated successfully"
|
529
|
+
rescue => e
|
530
|
+
redirect "/banner_config?error=Failed to save banner configuration: #{e.message}"
|
531
|
+
end
|
532
|
+
end
|
533
|
+
|
534
|
+
# Navbar configuration page
|
535
|
+
get '/navbar_config' do
|
536
|
+
@current_view = @api&.current_view
|
537
|
+
if @current_view.nil?
|
538
|
+
redirect "/?error=No view selected. Please select a view first."
|
539
|
+
return
|
540
|
+
end
|
541
|
+
|
542
|
+
# Get current navbar config
|
543
|
+
navbar_file = @api.root/"views"/@current_view.name/"config"/"navbar.txt"
|
544
|
+
@navbar_config = File.exist?(navbar_file) ? File.read(navbar_file).strip : ""
|
545
|
+
|
546
|
+
# Generate current navbar preview
|
547
|
+
begin
|
548
|
+
view = @api.lookup_view(@current_view.name)
|
549
|
+
@navbar_preview = view.build_nav(nil) # nil = use default navbar.txt
|
550
|
+
rescue => e
|
551
|
+
@navbar_preview = "<p>Error generating navbar: #{e.message}</p>"
|
552
|
+
end
|
553
|
+
|
554
|
+
erb :navbar_config
|
555
|
+
end
|
556
|
+
|
557
|
+
# Add item (top-level link or parent)
|
558
|
+
post '/navbar_config/add_item' do
|
559
|
+
@current_view = @api&.current_view
|
560
|
+
if @current_view.nil?
|
561
|
+
redirect "/?error=No view selected. Please select a view first."
|
562
|
+
return
|
563
|
+
end
|
564
|
+
|
565
|
+
begin
|
566
|
+
label = params[:label]&.strip
|
567
|
+
filename = params[:filename]&.strip
|
568
|
+
action = params[:action]
|
569
|
+
|
570
|
+
if label.nil? || label.empty?
|
571
|
+
redirect "/navbar_config?error=Label is required"
|
572
|
+
return
|
573
|
+
end
|
574
|
+
|
575
|
+
# Read current navbar config
|
576
|
+
navbar_file = @api.root/"views"/@current_view.name/"config"/"navbar.txt"
|
577
|
+
current_config = File.exist?(navbar_file) ? File.read(navbar_file).strip : ""
|
578
|
+
|
579
|
+
# Add new item based on action
|
580
|
+
if action == "link"
|
581
|
+
if filename.nil? || filename.empty?
|
582
|
+
redirect "/navbar_config?error=Filename is required for top-level links"
|
583
|
+
return
|
584
|
+
end
|
585
|
+
new_line = "-#{label} #{filename}"
|
586
|
+
message = "Added #{label} as top-level link"
|
587
|
+
else
|
588
|
+
new_line = "=#{label}"
|
589
|
+
message = "Added #{label} as parent"
|
590
|
+
end
|
591
|
+
|
592
|
+
# Append to config
|
593
|
+
updated_config = current_config.empty? ? new_line : "#{current_config.rstrip}\n#{new_line}"
|
594
|
+
|
595
|
+
# Save the updated configuration
|
596
|
+
FileUtils.mkdir_p(File.dirname(navbar_file))
|
597
|
+
File.write(navbar_file, updated_config.rstrip + "\n")
|
598
|
+
|
599
|
+
redirect "/navbar_config?message=#{message}"
|
600
|
+
rescue => e
|
601
|
+
redirect "/navbar_config?error=Failed to add item: #{e.message}"
|
602
|
+
end
|
603
|
+
end
|
604
|
+
|
605
|
+
# Add child to parent
|
606
|
+
post '/navbar_config/add_child' do
|
607
|
+
@current_view = @api&.current_view
|
608
|
+
if @current_view.nil?
|
609
|
+
redirect "/?error=No view selected. Please select a view first."
|
610
|
+
return
|
611
|
+
end
|
612
|
+
|
613
|
+
begin
|
614
|
+
parent = params[:parent]&.strip
|
615
|
+
label = params[:label]&.strip
|
616
|
+
filename = params[:filename]&.strip
|
617
|
+
|
618
|
+
if parent.nil? || parent.empty?
|
619
|
+
redirect "/navbar_config?error=Parent is required"
|
620
|
+
return
|
621
|
+
end
|
622
|
+
|
623
|
+
if label.nil? || label.empty?
|
624
|
+
redirect "/navbar_config?error=Label is required"
|
625
|
+
return
|
626
|
+
end
|
627
|
+
|
628
|
+
if filename.nil? || filename.empty?
|
629
|
+
redirect "/navbar_config?error=Filename is required"
|
630
|
+
return
|
631
|
+
end
|
632
|
+
|
633
|
+
# Read current navbar config
|
634
|
+
navbar_file = @api.root/"views"/@current_view.name/"config"/"navbar.txt"
|
635
|
+
current_config = File.exist?(navbar_file) ? File.read(navbar_file).strip : ""
|
636
|
+
|
637
|
+
# Find the parent and add child after it
|
638
|
+
lines = current_config.lines
|
639
|
+
new_lines = []
|
640
|
+
parent_found = false
|
641
|
+
|
642
|
+
lines.each do |line|
|
643
|
+
new_lines << line
|
644
|
+
if line.strip == "=#{parent}"
|
645
|
+
parent_found = true
|
646
|
+
# Add child on next line
|
647
|
+
new_lines << " #{label} #{filename}\n"
|
648
|
+
end
|
649
|
+
end
|
650
|
+
|
651
|
+
if !parent_found
|
652
|
+
redirect "/navbar_config?error=Parent '#{parent}' not found"
|
653
|
+
return
|
654
|
+
end
|
655
|
+
|
656
|
+
# Save the updated configuration
|
657
|
+
FileUtils.mkdir_p(File.dirname(navbar_file))
|
658
|
+
File.write(navbar_file, new_lines.join.rstrip + "\n")
|
659
|
+
|
660
|
+
redirect "/navbar_config?message=Added #{label} as child of #{parent}"
|
661
|
+
rescue => e
|
662
|
+
redirect "/navbar_config?error=Failed to add child: #{e.message}"
|
663
|
+
end
|
664
|
+
end
|
665
|
+
|
666
|
+
# Save direct edit of navbar config
|
667
|
+
post '/navbar_config/save_direct' do
|
668
|
+
@current_view = @api&.current_view
|
669
|
+
if @current_view.nil?
|
670
|
+
redirect "/?error=No view selected. Please select a view first."
|
671
|
+
return
|
672
|
+
end
|
673
|
+
|
674
|
+
begin
|
675
|
+
config = params[:config]&.strip
|
676
|
+
if config.nil?
|
677
|
+
redirect "/navbar_config?error=Configuration is required"
|
678
|
+
return
|
679
|
+
end
|
680
|
+
|
681
|
+
# Save the configuration
|
682
|
+
navbar_file = @api.root/"views"/@current_view.name/"config"/"navbar.txt"
|
683
|
+
FileUtils.mkdir_p(File.dirname(navbar_file))
|
684
|
+
File.write(navbar_file, config.rstrip + "\n")
|
685
|
+
|
686
|
+
# Check for missing pages and create them
|
687
|
+
pages_created = []
|
688
|
+
pages_dir = @api.root/"views"/@current_view.name/"pages"
|
689
|
+
FileUtils.mkdir_p(pages_dir) unless Dir.exist?(pages_dir)
|
690
|
+
|
691
|
+
# Parse navbar config to find page filenames
|
692
|
+
config.lines.each do |line|
|
693
|
+
line = line.rstrip
|
694
|
+
next if line.empty? || line.start_with?('#')
|
695
|
+
|
696
|
+
# Check for top-level links (start with -)
|
697
|
+
if line.start_with?('-')
|
698
|
+
if line.include?(' ')
|
699
|
+
parts = line.split(/\s{2,}/, 2)
|
700
|
+
if parts.length >= 2
|
701
|
+
title = parts[0].strip
|
702
|
+
filename = parts[1].strip
|
703
|
+
next if filename.empty?
|
704
|
+
|
705
|
+
# Add .lt3 extension if no extension
|
706
|
+
filename += '.lt3' unless filename.include?('.')
|
707
|
+
|
708
|
+
# Check if page exists
|
709
|
+
page_file = pages_dir/filename
|
710
|
+
unless File.exist?(page_file)
|
711
|
+
content = ".page_title #{title}\n\n"
|
712
|
+
File.write(page_file, content)
|
713
|
+
pages_created << filename
|
714
|
+
end
|
715
|
+
end
|
716
|
+
end
|
717
|
+
# Check for child links (start with space)
|
718
|
+
elsif line.start_with?(' ')
|
719
|
+
if line.include?(' ')
|
720
|
+
parts = line.split(/\s{2,}/, 2)
|
721
|
+
if parts.length >= 2
|
722
|
+
title = parts[0].strip
|
723
|
+
filename = parts[1].strip
|
724
|
+
next if filename.empty?
|
725
|
+
|
726
|
+
# Add .lt3 extension if no extension
|
727
|
+
filename += '.lt3' unless filename.include?('.')
|
728
|
+
|
729
|
+
# Check if page exists
|
730
|
+
page_file = pages_dir/filename
|
731
|
+
unless File.exist?(page_file)
|
732
|
+
content = ".page_title #{title}\n\n"
|
733
|
+
File.write(page_file, content)
|
734
|
+
pages_created << filename
|
735
|
+
end
|
736
|
+
end
|
737
|
+
end
|
738
|
+
end
|
739
|
+
end
|
740
|
+
|
741
|
+
# Build success message
|
742
|
+
message = "Configuration saved successfully"
|
743
|
+
if pages_created.any?
|
744
|
+
message += ". Created missing pages: #{pages_created.join(', ')}"
|
745
|
+
end
|
746
|
+
|
747
|
+
redirect "/navbar_config?message=#{message}"
|
748
|
+
rescue => e
|
749
|
+
redirect "/navbar_config?error=Failed to save configuration: #{e.message}"
|
750
|
+
end
|
751
|
+
end
|
752
|
+
|
753
|
+
# Edit pages page
|
754
|
+
get '/edit_pages' do
|
755
|
+
@current_view = @api&.current_view
|
756
|
+
if @current_view.nil?
|
757
|
+
redirect "/?error=No view selected. Please select a view first."
|
758
|
+
return
|
759
|
+
end
|
760
|
+
|
761
|
+
# Get all pages in the current view
|
762
|
+
pages_dir = @api.root/"views"/@current_view.name/"pages"
|
763
|
+
@pages = []
|
764
|
+
|
765
|
+
if Dir.exist?(pages_dir)
|
766
|
+
Dir.glob(pages_dir/"*").each do |file|
|
767
|
+
next unless File.file?(file)
|
768
|
+
filename = File.basename(file)
|
769
|
+
content = File.read(file)
|
770
|
+
|
771
|
+
# Extract page title from .page_title directive
|
772
|
+
title = nil
|
773
|
+
if content.lines.first&.strip&.start_with?('.page_title')
|
774
|
+
title = content.lines.first.strip.sub('.page_title', '').strip
|
775
|
+
end
|
776
|
+
|
777
|
+
@pages << {
|
778
|
+
filename: filename,
|
779
|
+
title: title,
|
780
|
+
content: content,
|
781
|
+
empty: content.strip.empty?
|
782
|
+
}
|
783
|
+
end
|
784
|
+
end
|
785
|
+
|
786
|
+
# Sort pages alphabetically
|
787
|
+
@pages.sort_by! { |page| page[:filename] }
|
788
|
+
|
789
|
+
erb :edit_pages
|
790
|
+
end
|
791
|
+
|
792
|
+
# Save page content
|
793
|
+
post '/edit_pages/save' do
|
794
|
+
@current_view = @api&.current_view
|
795
|
+
if @current_view.nil?
|
796
|
+
redirect "/?error=No view selected. Please select a view first."
|
797
|
+
return
|
798
|
+
end
|
799
|
+
|
800
|
+
begin
|
801
|
+
filename = params[:filename]&.strip
|
802
|
+
content = params[:content]&.strip || ""
|
803
|
+
|
804
|
+
if filename.nil? || filename.empty?
|
805
|
+
redirect "/edit_pages?error=Filename is required"
|
806
|
+
return
|
807
|
+
end
|
808
|
+
|
809
|
+
# Save the page
|
810
|
+
pages_dir = @api.root/"views"/@current_view.name/"pages"
|
811
|
+
FileUtils.mkdir_p(pages_dir)
|
812
|
+
page_file = pages_dir/filename
|
813
|
+
File.write(page_file, content)
|
814
|
+
|
815
|
+
redirect "/edit_pages?message=Page '#{filename}' saved successfully"
|
816
|
+
rescue => e
|
817
|
+
redirect "/edit_pages?error=Failed to save page: #{e.message}"
|
818
|
+
end
|
819
|
+
end
|
820
|
+
|
821
|
+
# Per-view dashboard
|
822
|
+
get '/view/:name' do
|
823
|
+
view_name = params[:name]
|
824
|
+
|
825
|
+
begin
|
826
|
+
# Look up the view
|
827
|
+
view = @api.lookup_view(view_name)
|
828
|
+
if view.nil?
|
829
|
+
redirect "/?error=View '#{view_name}' not found"
|
830
|
+
return
|
831
|
+
end
|
832
|
+
|
833
|
+
# Set as current view
|
834
|
+
@api.view(view_name)
|
835
|
+
@current_view = @api.current_view
|
836
|
+
|
837
|
+
# Generate banner for display
|
838
|
+
begin
|
839
|
+
bsvg = Scriptorium::BannerSVG.new(view.title, view.subtitle)
|
840
|
+
svg_config_file = @api.root/"views"/view_name/"config"/"svg.txt"
|
841
|
+
if File.exist?(svg_config_file)
|
842
|
+
# Temporarily rename svg.txt to config.txt for BannerSVG compatibility
|
843
|
+
config_dir = @api.root/"views"/view_name/"config"
|
844
|
+
Dir.chdir(config_dir) do
|
845
|
+
if File.exist?("config.txt")
|
846
|
+
File.rename("config.txt", "config.txt.backup")
|
847
|
+
end
|
848
|
+
File.rename("svg.txt", "config.txt")
|
849
|
+
|
850
|
+
begin
|
851
|
+
bsvg.parse_header_svg
|
852
|
+
ensure
|
853
|
+
# Restore original files
|
854
|
+
File.rename("config.txt", "svg.txt")
|
855
|
+
if File.exist?("config.txt.backup")
|
856
|
+
File.rename("config.txt.backup", "config.txt")
|
857
|
+
end
|
858
|
+
end
|
859
|
+
end
|
860
|
+
else
|
861
|
+
bsvg.parse_header_svg
|
862
|
+
end
|
863
|
+
# Generate responsive SVG for web display
|
864
|
+
svg_html = bsvg.generate_svg
|
865
|
+
# Extract the SVG element and make it responsive
|
866
|
+
svg_match = svg_html.match(/<svg[^>]*>(.*)<\/svg>/m)
|
867
|
+
if svg_match
|
868
|
+
svg_content = svg_match[1]
|
869
|
+
# Calculate height based on aspect ratio (7.0 from config)
|
870
|
+
width = 800
|
871
|
+
height = (width / 7.0).to_i
|
872
|
+
@banner_svg = <<~HTML
|
873
|
+
<svg xmlns='http://www.w3.org/2000/svg'
|
874
|
+
width='100%' height='auto'
|
875
|
+
viewBox='0 0 #{width} #{height}'
|
876
|
+
preserveAspectRatio='xMidYMid meet'>
|
877
|
+
#{svg_content}
|
878
|
+
</svg>
|
879
|
+
HTML
|
880
|
+
else
|
881
|
+
@banner_svg = svg_html
|
882
|
+
end
|
883
|
+
rescue => e
|
884
|
+
@banner_svg = "<p>Error generating banner: #{e.message}</p>"
|
885
|
+
end
|
886
|
+
|
887
|
+
erb :view_dashboard
|
888
|
+
rescue => e
|
889
|
+
redirect "/?error=Failed to load view dashboard: #{e.message}"
|
890
|
+
end
|
891
|
+
end
|
892
|
+
|
893
|
+
# Advanced configuration page
|
894
|
+
get '/advanced_config' do
|
895
|
+
@current_view = @api&.current_view
|
896
|
+
if @current_view.nil?
|
897
|
+
redirect "/?error=No view selected. Please select a view first."
|
898
|
+
return
|
899
|
+
end
|
900
|
+
|
901
|
+
# Read status from status.txt file
|
902
|
+
config_dir = @api.root/"views"/@current_view.name/"config"
|
903
|
+
status_file = config_dir/"status.txt"
|
904
|
+
@configs = {}
|
905
|
+
|
906
|
+
if File.exist?(status_file)
|
907
|
+
status_content = File.read(status_file)
|
908
|
+
status_content.lines.each do |line|
|
909
|
+
line = line.strip
|
910
|
+
next if line.empty? || line.start_with?('#')
|
911
|
+
if line.include?(' ')
|
912
|
+
key, value = line.split(/\s+/, 2)
|
913
|
+
@configs[key.to_sym] = value == 'y'
|
914
|
+
end
|
915
|
+
end
|
916
|
+
else
|
917
|
+
# Default to all 'n' if status file doesn't exist
|
918
|
+
@configs = {
|
919
|
+
header: false,
|
920
|
+
banner: false,
|
921
|
+
navbar: false,
|
922
|
+
left: false,
|
923
|
+
right: false,
|
924
|
+
pages: false,
|
925
|
+
deploy: false
|
926
|
+
}
|
927
|
+
end
|
928
|
+
|
929
|
+
# Read layout to determine which containers exist
|
930
|
+
layout_file = config_dir/"layout.txt"
|
931
|
+
@layout_containers = []
|
932
|
+
if File.exist?(layout_file)
|
933
|
+
layout_content = File.read(layout_file)
|
934
|
+
layout_content.lines.each do |line|
|
935
|
+
line = line.strip
|
936
|
+
next if line.empty? || line.start_with?('#')
|
937
|
+
if line.include?(' ')
|
938
|
+
container = line.split(/\s+/, 2)[0]
|
939
|
+
@layout_containers << container
|
940
|
+
else
|
941
|
+
@layout_containers << line
|
942
|
+
end
|
943
|
+
end
|
944
|
+
end
|
945
|
+
|
946
|
+
erb :advanced_config
|
947
|
+
end
|
948
|
+
|
949
|
+
# Header configuration page
|
950
|
+
get '/header_config' do
|
951
|
+
@current_view = @api&.current_view
|
952
|
+
if @current_view.nil?
|
953
|
+
redirect "/?error=No view selected. Please select a view first."
|
954
|
+
return
|
955
|
+
end
|
956
|
+
|
957
|
+
# Read current header config
|
958
|
+
header_file = @api.root/"views"/@current_view.name/"config"/"header.txt"
|
959
|
+
@current_config = ""
|
960
|
+
if File.exist?(header_file)
|
961
|
+
@current_config = File.read(header_file).strip
|
962
|
+
end
|
963
|
+
|
964
|
+
# Parse current settings
|
965
|
+
@banner_type = @current_config.include?("banner svg") ? "svg" : "image"
|
966
|
+
@navbar_enabled = @current_config.include?("navbar")
|
967
|
+
|
968
|
+
erb :header_config
|
969
|
+
end
|
970
|
+
|
971
|
+
# Update header configuration
|
972
|
+
post '/header_config' do
|
973
|
+
@current_view = @api&.current_view
|
974
|
+
if @current_view.nil?
|
975
|
+
redirect "/?error=No view selected. Please select a view first."
|
976
|
+
return
|
977
|
+
end
|
978
|
+
|
979
|
+
begin
|
980
|
+
banner_type = params[:banner_type] || "svg"
|
981
|
+
navbar_enabled = params[:navbar_enabled] == "1"
|
982
|
+
|
983
|
+
# Build header.txt content
|
984
|
+
header_content = []
|
985
|
+
header_content << "banner #{banner_type}"
|
986
|
+
header_content << "navbar" if navbar_enabled
|
987
|
+
|
988
|
+
# Save the header configuration
|
989
|
+
header_file = @api.root/"views"/@current_view.name/"config"/"header.txt"
|
990
|
+
FileUtils.mkdir_p(File.dirname(header_file))
|
991
|
+
File.write(header_file, header_content.join("\n") + "\n")
|
992
|
+
|
993
|
+
# Update status
|
994
|
+
update_config_status(@current_view.name, "header", true)
|
995
|
+
|
996
|
+
redirect "/advanced_config?message=Header configuration updated successfully"
|
997
|
+
rescue => e
|
998
|
+
redirect "/header_config?error=Failed to save header configuration: #{e.message}"
|
999
|
+
end
|
1000
|
+
end
|
1001
|
+
|
1002
|
+
# Deployment configuration page
|
1003
|
+
get '/deploy_config' do
|
1004
|
+
@current_view = @api&.current_view
|
1005
|
+
if @current_view.nil?
|
1006
|
+
redirect "/?error=No view selected. Please select a view first."
|
1007
|
+
return
|
1008
|
+
end
|
1009
|
+
|
1010
|
+
# Read current deployment config
|
1011
|
+
deploy_file = @api.root/"views"/@current_view.name/"config"/"deploy.txt"
|
1012
|
+
@deploy_config = ""
|
1013
|
+
if File.exist?(deploy_file)
|
1014
|
+
@deploy_config = File.read(deploy_file).strip
|
1015
|
+
end
|
1016
|
+
|
1017
|
+
erb :deploy_config
|
1018
|
+
end
|
1019
|
+
|
1020
|
+
# Update deployment configuration
|
1021
|
+
post '/deploy_config' do
|
1022
|
+
@current_view = @api&.current_view
|
1023
|
+
if @current_view.nil?
|
1024
|
+
redirect "/?error=No view selected. Please select a view first."
|
1025
|
+
return
|
1026
|
+
end
|
1027
|
+
|
1028
|
+
begin
|
1029
|
+
deploy_config = params[:deploy_config] || ""
|
1030
|
+
|
1031
|
+
# Save the deployment configuration
|
1032
|
+
deploy_file = @api.root/"views"/@current_view.name/"config"/"deploy.txt"
|
1033
|
+
FileUtils.mkdir_p(File.dirname(deploy_file))
|
1034
|
+
File.write(deploy_file, deploy_config + "\n")
|
1035
|
+
|
1036
|
+
# Update status
|
1037
|
+
update_config_status(@current_view.name, "deploy", true)
|
1038
|
+
|
1039
|
+
redirect "/advanced_config?message=Deployment configuration updated successfully"
|
1040
|
+
rescue => e
|
1041
|
+
redirect "/deploy_config?error=Failed to save deployment configuration: #{e.message}"
|
1042
|
+
end
|
1043
|
+
end
|
1044
|
+
|
1045
|
+
# Layout configuration page
|
1046
|
+
get '/layout_config' do
|
1047
|
+
@current_view = @api&.current_view
|
1048
|
+
if @current_view.nil?
|
1049
|
+
redirect "/?error=No view selected. Please select a view first."
|
1050
|
+
return
|
1051
|
+
end
|
1052
|
+
|
1053
|
+
# Read current layout config
|
1054
|
+
layout_file = @api.root/"views"/@current_view.name/"config"/"layout.txt"
|
1055
|
+
@layout_config = ""
|
1056
|
+
if File.exist?(layout_file)
|
1057
|
+
@layout_config = File.read(layout_file).strip
|
1058
|
+
end
|
1059
|
+
|
1060
|
+
erb :layout_config
|
1061
|
+
end
|
1062
|
+
|
1063
|
+
# Update layout configuration
|
1064
|
+
post '/layout_config' do
|
1065
|
+
@current_view = @api&.current_view
|
1066
|
+
if @current_view.nil?
|
1067
|
+
redirect "/?error=No view selected. Please select a view first."
|
1068
|
+
return
|
1069
|
+
end
|
1070
|
+
|
1071
|
+
begin
|
1072
|
+
layout_config = params[:layout_config] || ""
|
1073
|
+
|
1074
|
+
# Save the layout configuration
|
1075
|
+
layout_file = @api.root/"views"/@current_view.name/"config"/"layout.txt"
|
1076
|
+
FileUtils.mkdir_p(File.dirname(layout_file))
|
1077
|
+
File.write(layout_file, layout_config + "\n")
|
1078
|
+
|
1079
|
+
redirect "/advanced_config?message=Layout configuration updated successfully"
|
1080
|
+
rescue => e
|
1081
|
+
redirect "/layout_config?error=Failed to save layout configuration: #{e.message}"
|
1082
|
+
end
|
1083
|
+
end
|
1084
|
+
|
1085
|
+
# Serve global assets
|
1086
|
+
get '/assets/*' do
|
1087
|
+
asset_path = params[:splat].first
|
1088
|
+
asset_file = @api.root/"assets"/asset_path
|
1089
|
+
|
1090
|
+
if File.exist?(asset_file) && File.file?(asset_file)
|
1091
|
+
send_file asset_file
|
1092
|
+
else
|
1093
|
+
status 404
|
1094
|
+
"Asset not found"
|
1095
|
+
end
|
1096
|
+
end
|
1097
|
+
|
1098
|
+
# Serve view-specific assets
|
1099
|
+
get '/views/:view_name/assets/*' do
|
1100
|
+
view_name = params[:view_name]
|
1101
|
+
asset_path = params[:splat].first
|
1102
|
+
asset_file = @api.root/"views"/view_name/"assets"/asset_path
|
1103
|
+
|
1104
|
+
if File.exist?(asset_file) && File.file?(asset_file)
|
1105
|
+
send_file asset_file
|
1106
|
+
else
|
1107
|
+
status 404
|
1108
|
+
"Asset not found"
|
1109
|
+
end
|
1110
|
+
end
|
1111
|
+
|
1112
|
+
# Server status endpoint
|
1113
|
+
get '/status' do
|
1114
|
+
content_type :json
|
1115
|
+
{
|
1116
|
+
status: 'running',
|
1117
|
+
port: settings.port,
|
1118
|
+
current_view: @api.current_view&.name,
|
1119
|
+
repo_loaded: !@api.instance_variable_get(:@repo).nil?
|
1120
|
+
}.to_json
|
1121
|
+
end
|
1122
|
+
|
1123
|
+
# Asset management page
|
1124
|
+
get '/asset_management' do
|
1125
|
+
@current_view = @api&.current_view
|
1126
|
+
if @current_view.nil?
|
1127
|
+
redirect "/?error=No view selected. Please select a view first."
|
1128
|
+
return
|
1129
|
+
end
|
1130
|
+
|
1131
|
+
# Get global assets
|
1132
|
+
global_assets_dir = @api.root/"assets"
|
1133
|
+
@global_assets = []
|
1134
|
+
@library_assets = []
|
1135
|
+
|
1136
|
+
if Dir.exist?(global_assets_dir)
|
1137
|
+
Dir.glob(global_assets_dir/"*").each do |file|
|
1138
|
+
next unless File.file?(file)
|
1139
|
+
filename = File.basename(file)
|
1140
|
+
size = File.size(file)
|
1141
|
+
dimensions = get_image_dimensions(file)
|
1142
|
+
@global_assets << {
|
1143
|
+
filename: filename,
|
1144
|
+
size: size,
|
1145
|
+
path: file,
|
1146
|
+
dimensions: dimensions
|
1147
|
+
}
|
1148
|
+
end
|
1149
|
+
|
1150
|
+
# Get library assets
|
1151
|
+
library_dir = global_assets_dir/"library"
|
1152
|
+
if Dir.exist?(library_dir)
|
1153
|
+
Dir.glob(library_dir/"*").each do |file|
|
1154
|
+
next unless File.file?(file)
|
1155
|
+
filename = File.basename(file)
|
1156
|
+
size = File.size(file)
|
1157
|
+
dimensions = get_image_dimensions(file)
|
1158
|
+
@library_assets << {
|
1159
|
+
filename: filename,
|
1160
|
+
size: size,
|
1161
|
+
path: file,
|
1162
|
+
dimensions: dimensions
|
1163
|
+
}
|
1164
|
+
end
|
1165
|
+
end
|
1166
|
+
end
|
1167
|
+
|
1168
|
+
# Get view-specific assets
|
1169
|
+
view_assets_dir = @api.root/"views"/@current_view.name/"assets"
|
1170
|
+
@view_assets = []
|
1171
|
+
|
1172
|
+
if Dir.exist?(view_assets_dir)
|
1173
|
+
Dir.glob(view_assets_dir/"*").each do |file|
|
1174
|
+
next unless File.file?(file)
|
1175
|
+
filename = File.basename(file)
|
1176
|
+
size = File.size(file)
|
1177
|
+
dimensions = get_image_dimensions(file)
|
1178
|
+
@view_assets << {
|
1179
|
+
filename: filename,
|
1180
|
+
size: size,
|
1181
|
+
path: file,
|
1182
|
+
dimensions: dimensions
|
1183
|
+
}
|
1184
|
+
end
|
1185
|
+
end
|
1186
|
+
|
1187
|
+
# Sort all asset lists
|
1188
|
+
@global_assets.sort_by! { |asset| asset[:filename] }
|
1189
|
+
@library_assets.sort_by! { |asset| asset[:filename] }
|
1190
|
+
@view_assets.sort_by! { |asset| asset[:filename] }
|
1191
|
+
|
1192
|
+
erb :asset_management
|
1193
|
+
end
|
1194
|
+
|
1195
|
+
# Upload asset
|
1196
|
+
post '/asset_management/upload' do
|
1197
|
+
@current_view = @api&.current_view
|
1198
|
+
if @current_view.nil?
|
1199
|
+
redirect "/?error=No view selected. Please select a view first."
|
1200
|
+
return
|
1201
|
+
end
|
1202
|
+
|
1203
|
+
begin
|
1204
|
+
target = params[:target] # 'global', 'library', or 'view'
|
1205
|
+
file = params[:file]
|
1206
|
+
|
1207
|
+
if file.nil? || file[:tempfile].nil?
|
1208
|
+
redirect "/asset_management?error=No file selected"
|
1209
|
+
return
|
1210
|
+
end
|
1211
|
+
|
1212
|
+
filename = file[:filename]
|
1213
|
+
tempfile = file[:tempfile]
|
1214
|
+
|
1215
|
+
# Determine target directory
|
1216
|
+
case target
|
1217
|
+
when 'global'
|
1218
|
+
target_dir = @api.root/"assets"
|
1219
|
+
when 'library'
|
1220
|
+
target_dir = @api.root/"assets"/"library"
|
1221
|
+
when 'view'
|
1222
|
+
target_dir = @api.root/"views"/@current_view.name/"assets"
|
1223
|
+
else
|
1224
|
+
redirect "/asset_management?error=Invalid target"
|
1225
|
+
return
|
1226
|
+
end
|
1227
|
+
|
1228
|
+
# Create directory if it doesn't exist
|
1229
|
+
FileUtils.mkdir_p(target_dir)
|
1230
|
+
|
1231
|
+
# Save the file
|
1232
|
+
target_file = target_dir/filename
|
1233
|
+
FileUtils.cp(tempfile.path, target_file)
|
1234
|
+
|
1235
|
+
redirect "/asset_management?message=Asset '#{filename}' uploaded successfully to #{target}"
|
1236
|
+
rescue => e
|
1237
|
+
redirect "/asset_management?error=Failed to upload asset: #{e.message}"
|
1238
|
+
end
|
1239
|
+
end
|
1240
|
+
|
1241
|
+
# Copy asset from global to view
|
1242
|
+
post '/asset_management/copy' do
|
1243
|
+
@current_view = @api&.current_view
|
1244
|
+
if @current_view.nil?
|
1245
|
+
redirect "/?error=No view selected. Please select a view first."
|
1246
|
+
return
|
1247
|
+
end
|
1248
|
+
|
1249
|
+
begin
|
1250
|
+
source = params[:source] # 'global' or 'library'
|
1251
|
+
filename = params[:filename]
|
1252
|
+
|
1253
|
+
if filename.nil? || filename.empty?
|
1254
|
+
redirect "/asset_management?error=No filename specified"
|
1255
|
+
return
|
1256
|
+
end
|
1257
|
+
|
1258
|
+
# Determine source file
|
1259
|
+
case source
|
1260
|
+
when 'global'
|
1261
|
+
source_file = @api.root/"assets"/filename
|
1262
|
+
when 'library'
|
1263
|
+
source_file = @api.root/"assets"/"library"/filename
|
1264
|
+
else
|
1265
|
+
redirect "/asset_management?error=Invalid source"
|
1266
|
+
return
|
1267
|
+
end
|
1268
|
+
|
1269
|
+
unless File.exist?(source_file)
|
1270
|
+
redirect "/asset_management?error=Source file not found"
|
1271
|
+
return
|
1272
|
+
end
|
1273
|
+
|
1274
|
+
# Copy to view assets
|
1275
|
+
target_dir = @api.root/"views"/@current_view.name/"assets"
|
1276
|
+
FileUtils.mkdir_p(target_dir)
|
1277
|
+
target_file = target_dir/filename
|
1278
|
+
FileUtils.cp(source_file, target_file)
|
1279
|
+
|
1280
|
+
redirect "/asset_management?message=Asset '#{filename}' copied successfully to view"
|
1281
|
+
rescue => e
|
1282
|
+
redirect "/asset_management?error=Failed to copy asset: #{e.message}"
|
1283
|
+
end
|
1284
|
+
end
|
1285
|
+
|
1286
|
+
# Delete asset
|
1287
|
+
post '/asset_management/delete' do
|
1288
|
+
@current_view = @api&.current_view
|
1289
|
+
if @current_view.nil?
|
1290
|
+
redirect "/?error=No view selected. Please select a view first."
|
1291
|
+
return
|
1292
|
+
end
|
1293
|
+
|
1294
|
+
begin
|
1295
|
+
target = params[:target] # 'global', 'library', or 'view'
|
1296
|
+
filename = params[:filename]
|
1297
|
+
|
1298
|
+
if filename.nil? || filename.empty?
|
1299
|
+
redirect "/asset_management?error=No filename specified"
|
1300
|
+
return
|
1301
|
+
end
|
1302
|
+
|
1303
|
+
# Determine target file
|
1304
|
+
case target
|
1305
|
+
when 'global'
|
1306
|
+
target_file = @api.root/"assets"/filename
|
1307
|
+
when 'library'
|
1308
|
+
target_file = @api.root/"assets"/"library"/filename
|
1309
|
+
when 'view'
|
1310
|
+
target_file = @api.root/"views"/@current_view.name/"assets"/filename
|
1311
|
+
else
|
1312
|
+
redirect "/asset_management?error=Invalid target"
|
1313
|
+
return
|
1314
|
+
end
|
1315
|
+
|
1316
|
+
unless File.exist?(target_file)
|
1317
|
+
redirect "/asset_management?error=File not found"
|
1318
|
+
return
|
1319
|
+
end
|
1320
|
+
|
1321
|
+
# Delete the file
|
1322
|
+
File.delete(target_file)
|
1323
|
+
|
1324
|
+
redirect "/asset_management?message=Asset '#{filename}' deleted successfully"
|
1325
|
+
rescue => e
|
1326
|
+
redirect "/asset_management?error=Failed to delete asset: #{e.message}"
|
1327
|
+
end
|
1328
|
+
end
|
1329
|
+
|
1330
|
+
# Helper method to update status
|
1331
|
+
private def update_config_status(view_name, config_name, status)
|
1332
|
+
status_file = @api.root/"views"/view_name/"config"/"status.txt"
|
1333
|
+
return unless File.exist?(status_file)
|
1334
|
+
|
1335
|
+
content = File.read(status_file)
|
1336
|
+
lines = content.lines.map do |line|
|
1337
|
+
if line.strip.start_with?("#{config_name} ")
|
1338
|
+
"#{config_name} #{status ? 'y' : 'n'}\n"
|
1339
|
+
else
|
1340
|
+
line
|
1341
|
+
end
|
1342
|
+
end
|
1343
|
+
File.write(status_file, lines.join)
|
1344
|
+
end
|
1345
|
+
|
1346
|
+
# Helper method for formatting file sizes
|
1347
|
+
def number_to_human_size(bytes)
|
1348
|
+
return '0 Bytes' if bytes == 0
|
1349
|
+
k = 1024
|
1350
|
+
sizes = ['Bytes', 'KB', 'MB', 'GB']
|
1351
|
+
i = (Math.log(bytes) / Math.log(k)).floor
|
1352
|
+
"#{(bytes / k**i.to_f).round(2)} #{sizes[i]}"
|
1353
|
+
end
|
1354
|
+
|
1355
|
+
def get_image_dimensions(file_path)
|
1356
|
+
return nil unless File.exist?(file_path)
|
1357
|
+
|
1358
|
+
# Check if it's an image file
|
1359
|
+
image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp']
|
1360
|
+
return nil unless image_extensions.any? { |ext| file_path.downcase.end_with?(ext) }
|
1361
|
+
|
1362
|
+
# Check if FastImage is available
|
1363
|
+
return nil unless defined?(FastImage)
|
1364
|
+
|
1365
|
+
begin
|
1366
|
+
dimensions = FastImage.size(file_path)
|
1367
|
+
return dimensions ? "#{dimensions[0]}×#{dimensions[1]}" : nil
|
1368
|
+
rescue => e
|
1369
|
+
# If FastImage fails, return nil
|
1370
|
+
return nil
|
1371
|
+
end
|
1372
|
+
end
|
1373
|
+
end
|
1374
|
+
|
1375
|
+
# Start the server if this file is run directly
|
1376
|
+
if __FILE__ == $0
|
1377
|
+
ScriptoriumWeb.run!
|
1378
|
+
end
|