diff options
25 files changed, 117 insertions, 50 deletions
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0b40fb05b..d5eca6ffb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -12,6 +12,7 @@ class ApplicationController < ActionController::Base helper_method :current_account helper_method :current_session + helper_method :current_theme helper_method :single_user_mode? rescue_from ActionController::RoutingError, with: :not_found @@ -77,6 +78,11 @@ class ApplicationController < ActionController::Base @current_session ||= SessionActivation.find_by(session_id: cookies.signed['_session_id']) end + def current_theme + return Setting.default_settings['theme'] unless Themes.instance.names.include? current_user&.setting_theme + current_user.setting_theme + end + def cache_collection(raw, klass) return raw unless klass.respond_to?(:with_includes) diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index f107f2b16..207c7b324 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -41,6 +41,7 @@ class Settings::PreferencesController < ApplicationController :setting_auto_play_gif, :setting_system_font_ui, :setting_noindex, + :setting_theme, notification_emails: %i(follow follow_request reblog favourite mention digest), interactions: %i(must_be_follower must_be_following) ) diff --git a/app/javascript/glitch/components/local_settings/navigation/item/style.scss b/app/javascript/glitch/components/local_settings/navigation/item/style.scss index 505c86912..33d7d3744 100644 --- a/app/javascript/glitch/components/local_settings/navigation/item/style.scss +++ b/app/javascript/glitch/components/local_settings/navigation/item/style.scss @@ -1,4 +1,4 @@ -@import 'variables'; +@import 'styles/variables'; .glitch.local-settings__navigation__item { display: block; diff --git a/app/javascript/glitch/components/local_settings/navigation/style.scss b/app/javascript/glitch/components/local_settings/navigation/style.scss index 1cc39e3e9..a610a1212 100644 --- a/app/javascript/glitch/components/local_settings/navigation/style.scss +++ b/app/javascript/glitch/components/local_settings/navigation/style.scss @@ -1,4 +1,4 @@ -@import 'variables'; +@import 'styles/variables'; .glitch.local-settings__navigation { background: $primary-text-color; diff --git a/app/javascript/glitch/components/local_settings/page/item/style.scss b/app/javascript/glitch/components/local_settings/page/item/style.scss index e614030c0..da1941b99 100644 --- a/app/javascript/glitch/components/local_settings/page/item/style.scss +++ b/app/javascript/glitch/components/local_settings/page/item/style.scss @@ -1,4 +1,4 @@ -@import 'variables'; +@import 'styles/variables'; .glitch.local-settings__page__item { select { diff --git a/app/javascript/glitch/components/local_settings/page/style.scss b/app/javascript/glitch/components/local_settings/page/style.scss index 7269056c3..53c95ea40 100644 --- a/app/javascript/glitch/components/local_settings/page/style.scss +++ b/app/javascript/glitch/components/local_settings/page/style.scss @@ -1,4 +1,4 @@ -@import 'variables'; +@import 'styles/variables'; .glitch.local-settings__page { display: block; diff --git a/app/javascript/glitch/components/local_settings/style.scss b/app/javascript/glitch/components/local_settings/style.scss index 6f7fcbaa4..54fec47bd 100644 --- a/app/javascript/glitch/components/local_settings/style.scss +++ b/app/javascript/glitch/components/local_settings/style.scss @@ -1,4 +1,4 @@ -@import 'variables'; +@import 'styles/variables'; .glitch.local-settings { position: relative; diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index c06714dc1..aa94006c6 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -2,7 +2,7 @@ import loadPolyfills from '../mastodon/load_polyfills'; // import default stylesheet with variables require('font-awesome/css/font-awesome.css'); -require('mastodon-application-style'); +import 'styles/application'; require.context('../images/', true); diff --git a/app/javascript/packs/frontends/mastodon.js b/app/javascript/packs/frontends/mastodon.js deleted file mode 100644 index a983de36f..000000000 --- a/app/javascript/packs/frontends/mastodon.js +++ /dev/null @@ -1,16 +0,0 @@ -// This file replaces `app/javascript/packs/application.js` for use -// with multiple frontends. - -import loadPolyfills from '../../mastodon/load_polyfills'; - -// import default stylesheet with variables -require('font-awesome/css/font-awesome.css'); -require('mastodon-application-style'); - -require.context('../../images/', true); - -loadPolyfills().then(() => { - require('../../mastodon/main').default(); -}).catch(e => { - console.error(e); -}); diff --git a/app/javascript/styles/custom.scss b/app/javascript/styles/custom.scss deleted file mode 100644 index 97a981243..000000000 --- a/app/javascript/styles/custom.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'application'; diff --git a/app/javascript/themes/default/theme.yml b/app/javascript/themes/default/theme.yml new file mode 100644 index 000000000..6a7a872b4 --- /dev/null +++ b/app/javascript/themes/default/theme.yml @@ -0,0 +1,9 @@ +# (REQUIRED) Name must be unique across all installed themes. +name: default + +# (REQUIRED) The location of the pack file inside `pack_directory`. +pack: application.js + +# (OPTIONAL) The directory which contains the pack file. +# Defaults to the theme directory (`app/javascript/themes/[theme]`). +pack_directory: app/javascript/packs diff --git a/app/javascript/themes/spin/pack.js b/app/javascript/themes/spin/pack.js new file mode 100644 index 000000000..dab0e93a4 --- /dev/null +++ b/app/javascript/themes/spin/pack.js @@ -0,0 +1,2 @@ +import 'packs/application'; +import 'themes/spin/style'; diff --git a/app/javascript/themes/spin/style.scss b/app/javascript/themes/spin/style.scss new file mode 100644 index 000000000..1a9381fd0 --- /dev/null +++ b/app/javascript/themes/spin/style.scss @@ -0,0 +1,14 @@ +:root:root:root { + .button, .icon-button, .emoji-button, .account__avatar, .account__avatar-overlay { + animation: spin 4s linear infinite; + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/app/javascript/themes/spin/theme.yml b/app/javascript/themes/spin/theme.yml new file mode 100644 index 000000000..a684997dc --- /dev/null +++ b/app/javascript/themes/spin/theme.yml @@ -0,0 +1,2 @@ +name: spin +pack: pack.js \ No newline at end of file diff --git a/app/lib/themes.rb b/app/lib/themes.rb new file mode 100644 index 000000000..2dd188297 --- /dev/null +++ b/app/lib/themes.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'singleton' +require 'yaml' + +class Themes + include Singleton + + def initialize + result = Hash.new + Dir.glob(Rails.root.join('app', 'javascript', 'themes', '*', 'theme.yml')) do |path| + data = YAML.load_file(path) + if data['pack'] && data['name'] + result[data['name']] = data + end + end + @conf = result + end + + def names + @conf.keys + end +end diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index 62046ed72..3b156b98c 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -25,6 +25,7 @@ class UserSettingsDecorator user.settings['auto_play_gif'] = auto_play_gif_preference user.settings['system_font_ui'] = system_font_ui_preference user.settings['noindex'] = noindex_preference + user.settings['theme'] = theme_preference end def merged_notification_emails @@ -67,6 +68,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_noindex' end + def theme_preference + settings['setting_theme'] + end + def boolean_cast_setting(key) settings[key] == '1' end diff --git a/app/models/user.rb b/app/models/user.rb index 5e548c1ef..3bf069a31 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -110,6 +110,10 @@ class User < ApplicationRecord settings.noindex end + def setting_theme + settings.theme + end + def token_for_app(a) return nil if a.nil? || a.owner != self Doorkeeper::AccessToken diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index a13d0702b..3b4219c56 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -2,8 +2,8 @@ %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key} %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) - = javascript_pack_tag "frontends/#{@frontend}", integrity: true, crossorigin: 'anonymous' - = stylesheet_pack_tag "frontends/#{@frontend}", integrity: true, media: 'all' + = javascript_pack_tag "themes/#{current_theme}", integrity: true, crossorigin: 'anonymous' + = stylesheet_pack_tag "themes/#{current_theme}", integrity: true, media: 'all' .app-holder#mastodon{ data: { props: Oj.dump(default_props) } } %noscript diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index f42f92508..5efd538e4 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -5,6 +5,8 @@ = render 'shared/error_messages', object: current_user .fields-group + = f.input :setting_theme, collection: Themes.instance.names, label_method: lambda { |theme| safe_join([I18n.t("themes.#{theme}", default: theme)])}, wrapper: :with_label, include_blank: false + = f.input :locale, collection: I18n.available_locales, wrapper: :with_label, diff --git a/config/initializers/frontends.rb b/config/initializers/frontends.rb deleted file mode 100644 index 2cb68cc61..000000000 --- a/config/initializers/frontends.rb +++ /dev/null @@ -1,7 +0,0 @@ -# Be sure to restart your server when you modify this file. - -Rails.application.configure do - frontends = [] - Rails.root.join('app', 'javascript', 'packs', 'frontends').each_child(false) { |f| frontends.push f.to_s } - config.x.available_frontends = frontends -end diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index fb8524a24..f9d4e2e52 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -13,6 +13,7 @@ en: one: <span class="note-counter">1</span> character left other: <span class="note-counter">%{count}</span> characters left setting_noindex: Affects your public profile and status pages + setting_theme: Affects how Mastodon looks when you're logged in from any device. imports: data: CSV file exported from another Mastodon instance sessions: @@ -44,6 +45,7 @@ en: setting_noindex: Opt-out of search engine indexing setting_system_font_ui: Use system's default font setting_unfollow_modal: Show confirmation dialog before unfollowing someone + setting_theme: Site theme severity: Severity type: Import type username: Username diff --git a/config/settings.yml b/config/settings.yml index 39dfb8f55..3cd3307f4 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -24,6 +24,7 @@ defaults: &defaults auto_play_gif: false system_font_ui: false noindex: false + theme: 'default' notification_emails: follow: false reblog: false diff --git a/config/webpack/configuration.js b/config/webpack/configuration.js index 6ef484c3a..606eb97f1 100644 --- a/config/webpack/configuration.js +++ b/config/webpack/configuration.js @@ -1,13 +1,27 @@ // Common configuration for webpacker loaded from config/webpacker.yml -const { join, resolve } = require('path'); +const { dirname, join, resolve } = require('path'); const { env } = require('process'); const { safeLoad } = require('js-yaml'); const { readFileSync } = require('fs'); +const glob = require('glob'); const configPath = resolve('config', 'webpacker.yml'); const loadersDir = join(__dirname, 'loaders'); const settings = safeLoad(readFileSync(configPath), 'utf8')[env.NODE_ENV]; +const themeFiles = glob.sync('app/javascript/themes/*/theme.yml'); +const themes = {}; + +for (let i = 0; i < themeFiles.length; i++) { + const themeFile = themeFiles[i]; + const data = safeLoad(readFileSync(themeFile), 'utf8'); + if (!data.pack_directory) { + data.pack_directory = dirname(themeFile); + } + if (data.name && data.pack) { + themes[data.name] = data; + } +} function removeOuterSlashes(string) { return string.replace(/^\/*/, '').replace(/\/*$/, ''); @@ -29,6 +43,7 @@ const output = { module.exports = { settings, + themes, env, loadersDir, output, diff --git a/config/webpack/loaders/sass.js b/config/webpack/loaders/sass.js index 40e81b43b..96ad7abe8 100644 --- a/config/webpack/loaders/sass.js +++ b/config/webpack/loaders/sass.js @@ -9,7 +9,7 @@ module.exports = { { loader: 'css-loader', options: { minimize: env.NODE_ENV === 'production' } }, { loader: 'postcss-loader', options: { sourceMap: true } }, 'resolve-url-loader', - { loader: 'sass-loader', options: { includePaths: ['app/javascript/styles'] } }, + { loader: 'sass-loader', options: { includePaths: ['app/javascript'] } }, ], }), }; diff --git a/config/webpack/shared.js b/config/webpack/shared.js index be1b49421..ab925b020 100644 --- a/config/webpack/shared.js +++ b/config/webpack/shared.js @@ -1,13 +1,12 @@ // Note: You must restart bin/webpack-dev-server for changes to take effect -const { existsSync } = require('fs'); const webpack = require('webpack'); const { basename, dirname, join, relative, resolve } = require('path'); const { sync } = require('glob'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); const ManifestPlugin = require('webpack-manifest-plugin'); const extname = require('path-complete-extname'); -const { env, settings, output, loadersDir } = require('./configuration.js'); +const { env, settings, themes, output, loadersDir } = require('./configuration.js'); const localePackPaths = require('./generateLocalePacks'); const extensionGlob = `**/*{${settings.extensions.join(',')}}*`; @@ -18,17 +17,27 @@ const entryPacks = [...packPaths, ...localePackPaths].filter(path => path !== jo const customApplicationStyle = resolve(join(settings.source_path, 'styles/custom.scss')); const originalApplicationStyle = resolve(join(settings.source_path, 'styles/application.scss')); +const themePaths = Object.keys(themes).reduce( + (themePaths, name) => { + themeData = themes[name]; + themePaths[`themes/${name}`] = resolve(themeData.pack_directory, themeData.pack); + return themePaths; + }, {} +); + module.exports = { - entry: entryPacks.reduce( - (map, entry) => { - const localMap = map; - let namespace = relative(join(entryPath), dirname(entry)); - if (namespace === join('..', '..', '..', 'tmp', 'packs')) { - namespace = ''; // generated by generateLocalePacks.js - } - localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry); - return localMap; - }, {} + entry: Object.assign( + entryPacks.reduce( + (map, entry) => { + const localMap = map; + let namespace = relative(join(entryPath), dirname(entry)); + if (namespace === join('..', '..', '..', 'tmp', 'packs')) { + namespace = ''; // generated by generateLocalePacks.js + } + localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry); + return localMap; + }, {} + ), themePaths ), output: { @@ -59,10 +68,6 @@ module.exports = { ], resolve: { - alias: { - 'mastodon-application-style': existsSync(customApplicationStyle) ? - customApplicationStyle : originalApplicationStyle, - }, extensions: settings.extensions, modules: [ resolve(settings.source_path), |