]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
News UI overhaul and PDA notifications (#19610)
authorJulian Giebel <juliangiebel@live.de>
Tue, 27 Feb 2024 01:38:00 +0000 (02:38 +0100)
committerGitHub <noreply@github.com>
Tue, 27 Feb 2024 01:38:00 +0000 (21:38 -0400)
54 files changed:
Content.Client/CartridgeLoader/Cartridges/NewsReadUi.cs [deleted file]
Content.Client/CartridgeLoader/Cartridges/NewsReaderUi.cs [new file with mode: 0644]
Content.Client/CartridgeLoader/Cartridges/NewsReaderUiFragment.xaml [moved from Content.Client/CartridgeLoader/Cartridges/NewsReadUiFragment.xaml with 70% similarity]
Content.Client/CartridgeLoader/Cartridges/NewsReaderUiFragment.xaml.cs [moved from Content.Client/CartridgeLoader/Cartridges/NewsReadUiFragment.xaml.cs with 69% similarity]
Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml [new file with mode: 0644]
Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs [new file with mode: 0644]
Content.Client/MassMedia/Ui/MiniArticleCardControl.xaml [deleted file]
Content.Client/MassMedia/Ui/MiniArticleCardControl.xaml.cs [deleted file]
Content.Client/MassMedia/Ui/NewsArticleCard.xaml [new file with mode: 0644]
Content.Client/MassMedia/Ui/NewsArticleCard.xaml.cs [new file with mode: 0644]
Content.Client/MassMedia/Ui/NewsWriteBoundUserInterface.cs [deleted file]
Content.Client/MassMedia/Ui/NewsWriteMenu.xaml [deleted file]
Content.Client/MassMedia/Ui/NewsWriteMenu.xaml.cs [deleted file]
Content.Client/MassMedia/Ui/NewsWriterBoundUserInterface.cs [new file with mode: 0644]
Content.Client/MassMedia/Ui/NewsWriterMenu.xaml [new file with mode: 0644]
Content.Client/MassMedia/Ui/NewsWriterMenu.xaml.cs [new file with mode: 0644]
Content.Client/Paper/UI/PaperBoundUserInterface.cs
Content.Client/Stylesheets/StyleNano.cs
Content.Client/UserInterface/Controls/ConfirmButton.cs [new file with mode: 0644]
Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml.cs
Content.Server/CartridgeLoader/CartridgeLoaderSystem.cs
Content.Server/CartridgeLoader/Cartridges/NewsReaderCartridgeComponent.cs [moved from Content.Server/CartridgeLoader/Cartridges/NewsReadCartridgeComponent.cs with 51% similarity]
Content.Server/Content.Server.csproj
Content.Server/MassMedia/Components/NewsWriteComponent.cs [deleted file]
Content.Server/MassMedia/Components/NewsWriterComponent.cs [new file with mode: 0644]
Content.Server/MassMedia/Systems/NewsSystem.cs
Content.Server/PDA/PdaSystem.cs
Content.Server/PDA/Ringer/RingerSystem.cs
Content.Shared/CartridgeLoader/CartridgeComponent.cs
Content.Shared/CartridgeLoader/CartridgeLoaderComponent.cs
Content.Shared/CartridgeLoader/Cartridges/NewsReadUiMessageEvent.cs [deleted file]
Content.Shared/CartridgeLoader/Cartridges/NewsReaderUiMessageEvent.cs [new file with mode: 0644]
Content.Shared/CartridgeLoader/Cartridges/NewsReaderUiState.cs [moved from Content.Shared/CartridgeLoader/Cartridges/NewsReadUiState.cs with 60% similarity]
Content.Shared/CartridgeLoader/SharedCartridgeLoaderSystem.cs
Content.Shared/Chat/ChatChannel.cs
Content.Shared/MassMedia/Components/NewsWriterBuiMessages.cs [new file with mode: 0644]
Content.Shared/MassMedia/Components/SharedNewsWriteComponent.cs [deleted file]
Content.Shared/MassMedia/Components/StationNewsComponent.cs [new file with mode: 0644]
Content.Shared/MassMedia/Systems/SharedNewsSystem.cs
Resources/Locale/en-US/chat/ui/chat-box.ftl
Resources/Locale/en-US/generic.ftl
Resources/Locale/en-US/mass-media/news-ui.ftl
Resources/Locale/en-US/pda/Ringer/ringer-component.ftl
Resources/Locale/en-US/pda/pda-component.ftl
Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
Resources/Prototypes/Entities/Objects/Devices/cartridges.yml
Resources/Prototypes/Entities/Objects/Devices/pda.yml
Resources/Prototypes/Entities/Stations/base.yml
Resources/Prototypes/Entities/Stations/nanotrasen.yml
Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
Resources/Textures/Interface/Nano/button_small.svg [new file with mode: 0644]
Resources/Textures/Interface/Nano/button_small.svg.96dpi.png [new file with mode: 0644]
Resources/keybinds.yml

diff --git a/Content.Client/CartridgeLoader/Cartridges/NewsReadUi.cs b/Content.Client/CartridgeLoader/Cartridges/NewsReadUi.cs
deleted file mode 100644 (file)
index 6874e96..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-using Content.Client.UserInterface.Fragments;
-using Content.Shared.CartridgeLoader.Cartridges;
-using Content.Shared.CartridgeLoader;
-using Robust.Client.GameObjects;
-using Robust.Client.UserInterface;
-
-namespace Content.Client.CartridgeLoader.Cartridges;
-
-public sealed partial class NewsReadUi : UIFragment
-{
-    private NewsReadUiFragment? _fragment;
-
-    public override Control GetUIFragmentRoot()
-    {
-        return _fragment!;
-    }
-
-    public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
-    {
-        _fragment = new NewsReadUiFragment();
-
-        _fragment.OnNextButtonPressed += () =>
-        {
-            SendNewsReadMessage(NewsReadUiAction.Next, userInterface);
-        };
-        _fragment.OnPrevButtonPressed += () =>
-        {
-            SendNewsReadMessage(NewsReadUiAction.Prev, userInterface);
-        };
-        _fragment.OnNotificationSwithPressed += () =>
-        {
-            SendNewsReadMessage(NewsReadUiAction.NotificationSwith, userInterface);
-        };
-    }
-
-    public override void UpdateState(BoundUserInterfaceState state)
-    {
-        if (state is NewsReadBoundUserInterfaceState cast)
-            _fragment?.UpdateState(cast.Article, cast.TargetNum, cast.TotalNum, cast.NotificationOn);
-        else if (state is NewsReadEmptyBoundUserInterfaceState empty)
-            _fragment?.UpdateEmptyState(empty.NotificationOn);
-    }
-
-    private void SendNewsReadMessage(NewsReadUiAction action, BoundUserInterface userInterface)
-    {
-        var newsMessage = new NewsReadUiMessageEvent(action);
-        var message = new CartridgeUiMessage(newsMessage);
-        userInterface.SendMessage(message);
-    }
-}
diff --git a/Content.Client/CartridgeLoader/Cartridges/NewsReaderUi.cs b/Content.Client/CartridgeLoader/Cartridges/NewsReaderUi.cs
new file mode 100644 (file)
index 0000000..8ad665d
--- /dev/null
@@ -0,0 +1,54 @@
+using Content.Client.UserInterface.Fragments;
+using Content.Shared.CartridgeLoader.Cartridges;
+using Content.Shared.CartridgeLoader;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.CartridgeLoader.Cartridges;
+
+public sealed partial class NewsReaderUi : UIFragment
+{
+    private NewsReaderUiFragment? _fragment;
+
+    public override Control GetUIFragmentRoot()
+    {
+        return _fragment!;
+    }
+
+    public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
+    {
+        _fragment = new NewsReaderUiFragment();
+
+        _fragment.OnNextButtonPressed += () =>
+        {
+            SendNewsReaderMessage(NewsReaderUiAction.Next, userInterface);
+        };
+        _fragment.OnPrevButtonPressed += () =>
+        {
+            SendNewsReaderMessage(NewsReaderUiAction.Prev, userInterface);
+        };
+        _fragment.OnNotificationSwithPressed += () =>
+        {
+            SendNewsReaderMessage(NewsReaderUiAction.NotificationSwitch, userInterface);
+        };
+    }
+
+    public override void UpdateState(BoundUserInterfaceState state)
+    {
+        switch (state)
+        {
+            case NewsReaderBoundUserInterfaceState cast:
+                _fragment?.UpdateState(cast.Article, cast.TargetNum, cast.TotalNum, cast.NotificationOn);
+                break;
+            case NewsReaderEmptyBoundUserInterfaceState empty:
+                _fragment?.UpdateEmptyState(empty.NotificationOn);
+                break;
+        }
+    }
+
+    private void SendNewsReaderMessage(NewsReaderUiAction action, BoundUserInterface userInterface)
+    {
+        var newsMessage = new NewsReaderUiMessageEvent(action);
+        var message = new CartridgeUiMessage(newsMessage);
+        userInterface.SendMessage(message);
+    }
+}
similarity index 70%
rename from Content.Client/CartridgeLoader/Cartridges/NewsReadUiFragment.xaml
rename to Content.Client/CartridgeLoader/Cartridges/NewsReaderUiFragment.xaml
index 7431713ea8e0782597cc09820736ee338702f926..bd5879408ef5364becaec75ed62501b0d281cd51 100644 (file)
@@ -1,21 +1,30 @@
-<cartridges:NewsReadUiFragment xmlns:cartridges="clr-namespace:Content.Client.CartridgeLoader.Cartridges"
+<cartridges:NewsReaderUiFragment xmlns:cartridges="clr-namespace:Content.Client.CartridgeLoader.Cartridges"
                                xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
                                xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
-                               xmlns="https://spacestation14.io" Margin="1 0 2 0">
+                               xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
+                               xmlns="https://spacestation14.io"
+                               Margin="1 0 2 0"
+                               Orientation="Vertical"
+                               HorizontalExpand="True"
+                               VerticalExpand="True">
     <PanelContainer StyleClasses="BackgroundDark"></PanelContainer>
     <BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="5,5,5,5">
         <Button
             Name="Prev"
             MinWidth="64"
+            Disabled="True"
             HorizontalAlignment="Left"
-            Text="{Loc 'news-read-ui-past-text'}"
+            Text="{Loc 'news-read-ui-prev-text'}"
+            ToolTip="{Loc 'news-read-ui-prev-tooltip'}"
             Access="Public"
             HorizontalExpand="True"/>
         <Button
             Name="Next"
             MinWidth="64"
+            Disabled="True"
             HorizontalAlignment="Right"
-            Text="{Loc 'news-read-ui-next-text'}" />
+            Text="{Loc 'news-read-ui-next-text'}"
+            ToolTip="{Loc 'news-read-ui-next-tooltip'}"/>
     </BoxContainer>
     <controls:StripeBack Name="АrticleNameContainer">
         <PanelContainer>
     </PanelContainer>
     <BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="5,5,5,5">
         <Button
-            Name="NotificationSwith"
+            Name="NotificationSwitch"
+            ToolTip="{Loc news-reader-ui-mute-tooltip}"
             MinWidth="20"/>
         <RichTextLabel Margin="5,2,2,2" Name="ShareTime" VerticalAlignment="Top"/>
+        <customControls:VSeparator Margin="2 0"/>
         <RichTextLabel Margin="5,2,2,2" Name="Author" VerticalAlignment="Top" HorizontalAlignment="Right"/>
     </BoxContainer>
-</cartridges:NewsReadUiFragment>
+</cartridges:NewsReaderUiFragment>
similarity index 69%
rename from Content.Client/CartridgeLoader/Cartridges/NewsReadUiFragment.xaml.cs
rename to Content.Client/CartridgeLoader/Cartridges/NewsReaderUiFragment.xaml.cs
index df558429c934107bcd654c969cc3543002d356e3..f3b2d373d74e6daca9c0c96a6f42379834171da4 100644 (file)
@@ -7,23 +7,20 @@ using Robust.Client.UserInterface.XAML;
 namespace Content.Client.CartridgeLoader.Cartridges;
 
 [GenerateTypedNameReferences]
-public sealed partial class NewsReadUiFragment : BoxContainer
+public sealed partial class NewsReaderUiFragment : BoxContainer
 {
     public event Action? OnNextButtonPressed;
     public event Action? OnPrevButtonPressed;
 
     public event Action? OnNotificationSwithPressed;
 
-    public NewsReadUiFragment()
+    public NewsReaderUiFragment()
     {
         RobustXamlLoader.Load(this);
-        Orientation = LayoutOrientation.Vertical;
-        HorizontalExpand = true;
-        VerticalExpand = true;
 
         Next.OnPressed += _ => OnNextButtonPressed?.Invoke();
         Prev.OnPressed += _ => OnPrevButtonPressed?.Invoke();
-        NotificationSwith.OnPressed += _ => OnNotificationSwithPressed?.Invoke();
+        NotificationSwitch.OnPressed += _ => OnNotificationSwithPressed?.Invoke();
     }
 
     public void UpdateState(NewsArticle article, int targetNum, int totalNum, bool notificationOn)
@@ -33,17 +30,20 @@ public sealed partial class NewsReadUiFragment : BoxContainer
         ShareTime.Visible = true;
         Author.Visible = true;
 
-        PageName.Text = article.Name;
+        PageName.Text = article.Title;
         PageText.SetMarkup(article.Content);
 
         PageNum.Text = $"{targetNum}/{totalNum}";
 
-        NotificationSwith.Text = Loc.GetString(notificationOn ? "news-read-ui-notification-on" : "news-read-ui-notification-off");
+        NotificationSwitch.Text = Loc.GetString(notificationOn ? "news-read-ui-notification-on" : "news-read-ui-notification-off");
 
-        string shareTime = article.ShareTime.ToString("hh\\:mm\\:ss");
+        string shareTime = article.ShareTime.ToString(@"hh\:mm\:ss");
         ShareTime.SetMarkup(Loc.GetString("news-read-ui-time-prefix-text") + " " + shareTime);
 
         Author.SetMarkup(Loc.GetString("news-read-ui-author-prefix") + " " + (article.Author != null ? article.Author : Loc.GetString("news-read-ui-no-author")));
+
+        Prev.Disabled = targetNum <= 1;
+        Next.Disabled = targetNum >= totalNum;
     }
 
     public void UpdateEmptyState(bool notificationOn)
@@ -55,6 +55,6 @@ public sealed partial class NewsReadUiFragment : BoxContainer
 
         PageName.Text = Loc.GetString("news-read-ui-not-found-text");
 
-        NotificationSwith.Text = Loc.GetString(notificationOn ? "news-read-ui-notification-on" : "news-read-ui-notification-off");
+        NotificationSwitch.Text = Loc.GetString(notificationOn ? "news-read-ui-notification-on" : "news-read-ui-notification-off");
     }
 }
diff --git a/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml b/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml
new file mode 100644 (file)
index 0000000..02aec7e
--- /dev/null
@@ -0,0 +1,71 @@
+<Control xmlns="https://spacestation14.io"
+         xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+         MouseFilter="Stop">
+    <PanelContainer StyleClasses="BackgroundOpenLeft"/>
+    <PanelContainer>
+        <PanelContainer.PanelOverride>
+            <graphics:StyleBoxFlat BorderColor="#25252A" BorderThickness="0 0 0 3"/>
+        </PanelContainer.PanelOverride>
+    </PanelContainer>
+    <BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
+        <Control Margin="0 0 1 0">
+            <PanelContainer StyleClasses="WindowHeadingBackground" />
+            <BoxContainer Margin="4 2 8 0" Orientation="Horizontal">
+                <Label Name="Title" Text="{Loc news-write-ui-new-article}"
+                       HorizontalExpand="True" VAlign="Center" StyleClasses="FancyWindowTitle" />
+            </BoxContainer>
+        </Control>
+        <PanelContainer StyleClasses="LowDivider" Margin="0 0 1 0"/>
+        <BoxContainer Orientation="Horizontal">
+            <Label Text="Title:" Margin="17 10 0 9" VerticalAlignment="Center"/>
+            <LineEdit Name="TitleField" Margin="6 10 0 9" MinWidth="260" MinHeight="23" Access="Public"/>
+            <Control HorizontalExpand="True" />
+            <Label Name="RichTextInfoLabel" Text="?" MouseFilter="Pass" Margin="14 0" StyleClasses="LabelSecondaryColor"/>
+        </BoxContainer>
+        <Control Name="TextEditPanel" VerticalExpand="True" Margin="11 0 11 0">
+            <PanelContainer>
+                <PanelContainer.PanelOverride>
+                    <graphics:StyleBoxFlat BackgroundColor="#202023" BorderThickness="1" BorderColor="#3B3E56"/>
+                </PanelContainer.PanelOverride>
+            </PanelContainer>
+            <TextEdit Name="ContentField" Margin="0 1" Access="Public"/>
+        </Control>
+        <Control Name="PreviewPanel" Visible="False" VerticalExpand="True" Margin="11 0 11 0">
+            <PanelContainer>
+                <PanelContainer.PanelOverride>
+                    <graphics:StyleBoxFlat BorderThickness="1" BorderColor="#3B3E56"/>
+                </PanelContainer.PanelOverride>
+            </PanelContainer>
+            <ScrollContainer HScrollEnabled="True">
+                <RichTextLabel Name="PreviewLabel" VerticalAlignment="Top" Margin="9 3" MaxWidth="360"/>
+            </ScrollContainer>
+        </Control>
+        <BoxContainer Orientation="Horizontal" Margin="12 5 12 8">
+            <Control>
+                <Button Name="ButtonCancel" SetHeight="32" SetWidth="85"
+                        StyleClasses="ButtonColorRed" Text="{Loc news-write-ui-cancel-text}"/>
+            </Control>
+            <Control HorizontalExpand="True"/>
+            <BoxContainer Orientation="Horizontal">
+                <Button Name="ButtonPreview" SetHeight="32" SetWidth="85"
+                        StyleClasses="OpenRight" Text="{Loc news-write-ui-preview-text}"/>
+                <Button Name="ButtonPublish" SetHeight="32" SetWidth="85" Text="{Loc news-write-ui-publish-text}" Access="Public"/>
+            </BoxContainer>
+        </BoxContainer>
+    </BoxContainer>
+    <PanelContainer>
+        <PanelContainer.PanelOverride>
+            <graphics:StyleBoxFlat BorderThickness="2 0 0 0" BorderColor="#1d1d22"/>
+        </PanelContainer.PanelOverride>
+    </PanelContainer>
+    <PanelContainer HorizontalAlignment="Left" VerticalAlignment="Top" SetHeight="27" SetWidth="2">
+        <PanelContainer.PanelOverride>
+            <graphics:StyleBoxFlat BorderColor="#2a2a2d" BorderThickness="0 0 0 2"/>
+        </PanelContainer.PanelOverride>
+    </PanelContainer>
+    <PanelContainer HorizontalAlignment="Left" VerticalAlignment="Top" SetHeight="25" SetWidth="2">
+        <PanelContainer.PanelOverride>
+            <graphics:StyleBoxFlat BackgroundColor="#1b1b1f"/>
+        </PanelContainer.PanelOverride>
+    </PanelContainer>
+</Control>
diff --git a/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs b/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs
new file mode 100644 (file)
index 0000000..2918faa
--- /dev/null
@@ -0,0 +1,109 @@
+using Content.Client.Message;
+using Content.Client.Stylesheets;
+using Content.Shared.MassMedia.Systems;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Utility;
+
+namespace Content.Client.MassMedia.Ui;
+
+[GenerateTypedNameReferences]
+public sealed partial class ArticleEditorPanel : Control
+{
+    public event Action? PublishButtonPressed;
+
+    private bool _preview;
+
+    public ArticleEditorPanel()
+    {
+        RobustXamlLoader.Load(this);
+
+        ButtonPublish.StyleClasses.Add(StyleBase.ButtonOpenLeft);
+        ButtonPublish.StyleClasses.Add(StyleNano.StyleClassButtonColorGreen);
+
+        ContentField.GetChild(0).Margin = new Thickness(9, 3);
+        // Customize scrollbar width and margin. This is not possible in xaml
+        var scrollbar = ContentField.GetChild(1);
+        scrollbar.SetWidth = 6f;
+        scrollbar.Margin = new Thickness(9, 0, 2 , 0);
+
+        RichTextInfoLabel.TooltipSupplier = sender =>
+        {
+            var label = new RichTextLabel();
+            label.SetMarkup(Loc.GetString("news-write-ui-richtext-tooltip"));
+
+            var tooltip = new Tooltip();
+            tooltip.GetChild(0).Children.Clear();
+            tooltip.GetChild(0).Children.Add(label);
+
+            return tooltip;
+        };
+
+        ButtonPreview.OnPressed += OnPreview;
+        ButtonCancel.OnPressed += OnCancel;
+        ButtonPublish.OnPressed += OnPublish;
+
+        TitleField.OnTextChanged += args => OnTextChanged(args.Text.Length, args.Control, SharedNewsSystem.MaxTitleLength);
+        ContentField.OnTextChanged += args => OnTextChanged(Rope.CalcTotalLength(args.TextRope), args.Control, SharedNewsSystem.MaxContentLength);
+    }
+
+    private void OnTextChanged(long length, Control control, long maxLength)
+    {
+        if (length > maxLength)
+        {
+            control.ModulateSelfOverride = Color.Red;
+            control.ToolTip = Loc.GetString("news-writer-text-length-exceeded");
+        }
+        else
+        {
+            control.ModulateSelfOverride = null;
+            control.ToolTip = string.Empty;
+        }
+    }
+
+    private void OnPreview(BaseButton.ButtonEventArgs eventArgs)
+    {
+        _preview = !_preview;
+
+        TextEditPanel.Visible = !_preview;
+        PreviewPanel.Visible = _preview;
+        PreviewLabel.SetMarkup(Rope.Collapse(ContentField.TextRope));
+    }
+
+    private void OnCancel(BaseButton.ButtonEventArgs eventArgs)
+    {
+        Reset();
+        Visible = false;
+    }
+
+    private void OnPublish(BaseButton.ButtonEventArgs eventArgs)
+    {
+        PublishButtonPressed?.Invoke();
+        Reset();
+        Visible = false;
+    }
+
+    private void Reset()
+    {
+        _preview = false;
+        TextEditPanel.Visible = true;
+        PreviewPanel.Visible = false;
+        PreviewLabel.SetMarkup("");
+        TitleField.Text = "";
+        ContentField.TextRope = Rope.Leaf.Empty;
+    }
+
+    protected override void Dispose(bool disposing)
+    {
+        base.Dispose(disposing);
+        if (!disposing)
+            return;
+
+        ButtonPreview.OnPressed -= OnPreview;
+        ButtonCancel.OnPressed -= OnCancel;
+        ButtonPublish.OnPressed -= OnPublish;
+    }
+}
diff --git a/Content.Client/MassMedia/Ui/MiniArticleCardControl.xaml b/Content.Client/MassMedia/Ui/MiniArticleCardControl.xaml
deleted file mode 100644 (file)
index ede51fc..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-<Control xmlns="https://spacestation14.io"
-         xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client">
-    <BoxContainer Orientation="Vertical" HorizontalExpand="True" Margin="0 0 0 12">
-        <BoxContainer Orientation="Horizontal" HorizontalExpand="True" VerticalExpand="True">
-            <PanelContainer HorizontalExpand="True" VerticalExpand="True">
-                <PanelContainer.PanelOverride>
-                    <gfx:StyleBoxFlat BackgroundColor="#4c6530"/>
-                </PanelContainer.PanelOverride>
-                <Label Name="NameLabel" Margin="6 6 6 6" HorizontalAlignment="Center"/>
-            </PanelContainer>
-        </BoxContainer>
-        <BoxContainer Orientation="Horizontal" HorizontalExpand="True">
-            <PanelContainer HorizontalExpand="True" VerticalExpand="True">
-                <PanelContainer.PanelOverride>
-                    <gfx:StyleBoxFlat BackgroundColor="#33333f"/>
-                </PanelContainer.PanelOverride>
-                <RichTextLabel Name="Author" HorizontalExpand="True" VerticalAlignment="Bottom" Margin="6 6 6 6"/>
-                <Button Name="Delete"
-                        Text="{Loc 'news-write-ui-delete-text'}"
-                        HorizontalAlignment="Right"
-                        Margin="8 6 6 6"
-                        Access="Public"/>
-            </PanelContainer>
-        </BoxContainer>
-    </BoxContainer>
-</Control>
diff --git a/Content.Client/MassMedia/Ui/MiniArticleCardControl.xaml.cs b/Content.Client/MassMedia/Ui/MiniArticleCardControl.xaml.cs
deleted file mode 100644 (file)
index 3c5076e..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-using Content.Client.Message;
-using Content.Shared.Research.Prototypes;
-using Robust.Client.AutoGenerated;
-using Robust.Client.GameObjects;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
-
-namespace Content.Client.MassMedia.Ui;
-
-[GenerateTypedNameReferences]
-public sealed partial class MiniArticleCardControl : Control
-{
-    public Action? OnDeletePressed;
-    public int ArticleNum;
-
-    public MiniArticleCardControl(string name, string author)
-    {
-        RobustXamlLoader.Load(this);
-
-        NameLabel.Text = name;
-        Author.SetMarkup(author);
-
-        Delete.OnPressed += _ => OnDeletePressed?.Invoke();
-    }
-}
diff --git a/Content.Client/MassMedia/Ui/NewsArticleCard.xaml b/Content.Client/MassMedia/Ui/NewsArticleCard.xaml
new file mode 100644 (file)
index 0000000..abfc1cb
--- /dev/null
@@ -0,0 +1,29 @@
+<Control xmlns="https://spacestation14.io"
+         xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+         xmlns:system="clr-namespace:System;assembly=System.Runtime"
+         xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+         Margin="0 0 0 8">
+    <PanelContainer StyleClasses="AngleRect" ModulateSelfOverride="#2b2b31"/>
+    <BoxContainer Orientation="Vertical" SetHeight="60">
+        <Control HorizontalExpand="True" SetHeight="27">
+            <PanelContainer>
+                <PanelContainer.PanelOverride>
+                    <gfx:StyleBoxFlat BorderColor="#3B3E56" BorderThickness="0 0 0 1"/>
+                </PanelContainer.PanelOverride>
+            </PanelContainer>
+            <Label Name="TitleLabel" Margin="12 0 6 0" HorizontalAlignment="Left"/>
+        </Control>
+        <BoxContainer Orientation="Horizontal" HorizontalExpand="True">
+            <Label FontColorOverride="#b1b1b2" StyleClasses="LabelSmall" Name="AuthorLabel" Margin="14 6 6 6"/>
+            <Control HorizontalExpand="True"/>
+            <Label FontColorOverride="#b1b1b2" StyleClasses="LabelSmall" Name="PublishTimeLabel" Margin="6 6 6 6"/>
+            <controls:ConfirmButton Name="DeleteButton" Text="{Loc news-write-ui-delete-text}"
+                    HorizontalAlignment="Right" Margin="8 6 6 6" SetHeight="19" SetWidth="52" Access="Public">
+                <Button.StyleClasses>
+                    <system:String>ButtonSmall</system:String>
+                    <system:String>ButtonColorRed</system:String>
+                </Button.StyleClasses>
+            </controls:ConfirmButton>
+        </BoxContainer>
+    </BoxContainer>
+</Control>
diff --git a/Content.Client/MassMedia/Ui/NewsArticleCard.xaml.cs b/Content.Client/MassMedia/Ui/NewsArticleCard.xaml.cs
new file mode 100644 (file)
index 0000000..f6fa3e0
--- /dev/null
@@ -0,0 +1,47 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.MassMedia.Ui;
+
+[GenerateTypedNameReferences]
+public sealed partial class NewsArticleCard : Control
+{
+    private string? _authorMarkup;
+    private TimeSpan? _publicationTime;
+
+    public Action? OnDeletePressed;
+    public int ArtcileNumber;
+
+    public string? Title
+    {
+        get => TitleLabel.Text;
+        set => TitleLabel.Text = value?.Length <= 30 ? value : $"{value?[..30]}...";
+    }
+
+    public string? Author
+    {
+        get => _authorMarkup;
+        set
+        {
+            _authorMarkup = value;
+            AuthorLabel.Text = _authorMarkup ?? "";
+        }
+    }
+
+    public TimeSpan? PublicationTime
+    {
+        get => _publicationTime;
+        set
+        {
+            _publicationTime = value;
+            PublishTimeLabel.Text = value?.ToString(@"hh\:mm\:ss") ?? "";
+        }
+    }
+
+    public NewsArticleCard()
+    {
+        RobustXamlLoader.Load(this);
+        DeleteButton.OnPressed += _ => OnDeletePressed?.Invoke();
+    }
+}
diff --git a/Content.Client/MassMedia/Ui/NewsWriteBoundUserInterface.cs b/Content.Client/MassMedia/Ui/NewsWriteBoundUserInterface.cs
deleted file mode 100644 (file)
index d1d61d5..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-using JetBrains.Annotations;
-using Content.Shared.MassMedia.Components;
-using Content.Shared.MassMedia.Systems;
-using Robust.Shared.Utility;
-
-namespace Content.Client.MassMedia.Ui
-{
-    [UsedImplicitly]
-    public sealed class NewsWriteBoundUserInterface : BoundUserInterface
-    {
-        [ViewVariables]
-        private NewsWriteMenu? _menu;
-
-        [ViewVariables]
-        private string _windowName = Loc.GetString("news-read-ui-default-title");
-
-        public NewsWriteBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
-        {
-        }
-
-        protected override void Open()
-        {
-            _menu = new NewsWriteMenu(_windowName);
-
-            _menu.OpenCentered();
-            _menu.OnClose += Close;
-
-            _menu.ShareButtonPressed += OnShareButtonPressed;
-            _menu.DeleteButtonPressed += OnDeleteButtonPressed;
-
-            SendMessage(new NewsWriteArticlesRequestMessage());
-        }
-
-        protected override void Dispose(bool disposing)
-        {
-            base.Dispose(disposing);
-            if (!disposing)
-                return;
-
-            _menu?.Close();
-            _menu?.Dispose();
-        }
-
-        protected override void UpdateState(BoundUserInterfaceState state)
-        {
-            base.UpdateState(state);
-            if (_menu == null || state is not NewsWriteBoundUserInterfaceState cast)
-                return;
-
-            _menu.UpdateUI(cast.Articles, cast.ShareAvalible);
-        }
-
-        private void OnShareButtonPressed()
-        {
-            if (_menu == null || _menu.NameInput.Text.Length == 0)
-                return;
-
-            var stringContent = Rope.Collapse(_menu.ContentInput.TextRope);
-
-            if (stringContent.Length == 0)
-                return;
-
-            var stringName = _menu.NameInput.Text.Trim();
-            var name = stringName[..Math.Min(stringName.Length, (SharedNewsSystem.MaxNameLength))];
-            var content = stringContent[..Math.Min(stringContent.Length, (SharedNewsSystem.MaxArticleLength))];
-            _menu.ContentInput.TextRope = new Rope.Leaf(string.Empty);
-            _menu.NameInput.Text = string.Empty;
-            SendMessage(new NewsWriteShareMessage(name, content));
-        }
-
-        private void OnDeleteButtonPressed(int articleNum)
-        {
-            if (_menu == null)
-                return;
-
-            SendMessage(new NewsWriteDeleteMessage(articleNum));
-        }
-    }
-}
diff --git a/Content.Client/MassMedia/Ui/NewsWriteMenu.xaml b/Content.Client/MassMedia/Ui/NewsWriteMenu.xaml
deleted file mode 100644 (file)
index 08d113f..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-<DefaultWindow
-    xmlns="https://spacestation14.io"
-    xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
-    xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
-    xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
-    Title="{Loc 'news-write-ui-default-title'}"
-    MinSize="680 512"
-    SetSize="680 512">
-    <BoxContainer Orientation="Horizontal"
-                  HorizontalExpand="True"
-                  VerticalExpand="True">
-        <BoxContainer Orientation="Vertical"
-                      VerticalExpand="True"
-                      SizeFlagsStretchRatio="2"
-                      Margin="10 0 10 10"
-                      MinWidth="350">
-            <Label Text="{Loc 'news-write-ui-articles-label'}" HorizontalAlignment="Center"/>
-            <customControls:HSeparator StyleClasses="LowDivider" Margin="0 0 0 10"/>
-            <PanelContainer VerticalExpand="True">
-                <PanelContainer.PanelOverride>
-                    <gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
-                </PanelContainer.PanelOverride>
-                <ScrollContainer
-                    HScrollEnabled="False"
-                    HorizontalExpand="True"
-                    VerticalExpand="True">
-                    <BoxContainer
-                        Name="ArticleCardsContainer"
-                        Orientation="Vertical"
-                        VerticalExpand="True">
-                    </BoxContainer>
-                </ScrollContainer>
-            </PanelContainer>
-        </BoxContainer>
-        <BoxContainer Orientation="Vertical"
-                      VerticalExpand="True"
-                      HorizontalExpand="True"
-                      Margin="15 0 0 0">
-            <BoxContainer Orientation="Horizontal" HorizontalExpand="True">
-                <Label Text="{Loc 'news-write-ui-article-name-label'}"/>
-                <LineEdit Name="NameInput"
-                          MinSize="60 0"
-                          VerticalAlignment="Top"
-                          Margin="4 0 0 0"
-                          Access="Public"
-                          HorizontalExpand="True"/>
-            </BoxContainer>
-            <customControls:HSeparator StyleClasses="LowDivider" Margin="0 5 0 5"/>
-            <Label Text="{Loc 'news-write-ui-article-content-label'}" Margin="0 0 0 5"/>
-            <PanelContainer Name="InputContainer"
-                            VerticalAlignment="Stretch"
-                            VerticalExpand="True"
-                            HorizontalExpand="True">
-                <PanelContainer.PanelOverride>
-                    <gfx:StyleBoxFlat BackgroundColor="#333237"/>
-                </PanelContainer.PanelOverride>
-                <TextEdit Name="ContentInput" Access="Public" />
-            </PanelContainer>
-            <Button Name="Share"
-                    MinWidth="30"
-                    HorizontalAlignment="Left"
-                    Text="{Loc 'news-write-ui-share-text'}"
-                    Access="Public"
-                    Margin="0 4 4 4">
-            </Button>
-        </BoxContainer>
-    </BoxContainer>
-</DefaultWindow>
diff --git a/Content.Client/MassMedia/Ui/NewsWriteMenu.xaml.cs b/Content.Client/MassMedia/Ui/NewsWriteMenu.xaml.cs
deleted file mode 100644 (file)
index 89ab149..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-using Robust.Client.AutoGenerated;
-using Robust.Client.UserInterface.CustomControls;
-using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Prototypes;
-using Content.Shared.MassMedia.Systems;
-
-namespace Content.Client.MassMedia.Ui;
-
-[GenerateTypedNameReferences]
-public sealed partial class NewsWriteMenu : DefaultWindow
-{
-    public event Action? ShareButtonPressed;
-    public event Action<int>? DeleteButtonPressed;
-
-    public NewsWriteMenu(string name)
-    {
-        RobustXamlLoader.Load(this);
-        IoCManager.InjectDependencies(this);
-
-        if (Window != null)
-            Window.Title = name;
-
-        Share.OnPressed += _ => ShareButtonPressed?.Invoke();
-    }
-
-    public void UpdateUI(NewsArticle[] articles, bool shareAvalible)
-    {
-        ArticleCardsContainer.Children.Clear();
-
-        for (int i = 0; i < articles.Length; i++)
-        {
-            var article = articles[i];
-            var mini = new MiniArticleCardControl(article.Name, (article.Author != null ? article.Author : Loc.GetString("news-read-ui-no-author")));
-            mini.ArticleNum = i;
-            mini.OnDeletePressed += () => DeleteButtonPressed?.Invoke(mini.ArticleNum);
-
-            ArticleCardsContainer.AddChild(mini);
-        }
-
-        Share.Disabled = !shareAvalible;
-    }
-}
diff --git a/Content.Client/MassMedia/Ui/NewsWriterBoundUserInterface.cs b/Content.Client/MassMedia/Ui/NewsWriterBoundUserInterface.cs
new file mode 100644 (file)
index 0000000..80eca82
--- /dev/null
@@ -0,0 +1,84 @@
+using JetBrains.Annotations;
+using Content.Shared.MassMedia.Systems;
+using Content.Shared.MassMedia.Components;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Client.MassMedia.Ui;
+
+[UsedImplicitly]
+public sealed class NewsWriterBoundUserInterface : BoundUserInterface
+{
+    [Dependency] private readonly IGameTiming _gameTiming = default!;
+
+    [ViewVariables]
+    private NewsWriterMenu? _menu;
+
+    public NewsWriterBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+    {
+
+    }
+
+    protected override void Open()
+    {
+        _menu = new NewsWriterMenu(_gameTiming);
+
+        _menu.OpenCentered();
+        _menu.OnClose += Close;
+
+        _menu.ArticleEditorPanel.PublishButtonPressed += OnPublishButtonPressed;
+        _menu.DeleteButtonPressed += OnDeleteButtonPressed;
+
+        SendMessage(new NewsWriterArticlesRequestMessage());
+    }
+
+    protected override void Dispose(bool disposing)
+    {
+        base.Dispose(disposing);
+        if (!disposing)
+            return;
+
+        _menu?.Close();
+        _menu?.Dispose();
+    }
+
+    protected override void UpdateState(BoundUserInterfaceState state)
+    {
+        base.UpdateState(state);
+        if (state is not NewsWriterBoundUserInterfaceState cast)
+            return;
+
+        _menu?.UpdateUI(cast.Articles, cast.PublishEnabled, cast.NextPublish);
+    }
+
+    private void OnPublishButtonPressed()
+    {
+        var title = _menu?.ArticleEditorPanel.TitleField.Text.Trim() ?? "";
+        if (_menu == null || title.Length == 0)
+            return;
+
+        var stringContent = Rope.Collapse(_menu.ArticleEditorPanel.ContentField.TextRope).Trim();
+
+        if (stringContent.Length == 0)
+            return;
+
+        var name = title.Length <= SharedNewsSystem.MaxTitleLength
+            ? title
+            : $"{title[..(SharedNewsSystem.MaxTitleLength - 3)]}...";
+
+        var content = stringContent.Length <= SharedNewsSystem.MaxContentLength
+            ? stringContent
+            : $"{stringContent[..(SharedNewsSystem.MaxContentLength - 3)]}...";
+
+
+        SendMessage(new NewsWriterPublishMessage(name, content));
+    }
+
+    private void OnDeleteButtonPressed(int articleNum)
+    {
+        if (_menu == null)
+            return;
+
+        SendMessage(new NewsWriterDeleteMessage(articleNum));
+    }
+}
diff --git a/Content.Client/MassMedia/Ui/NewsWriterMenu.xaml b/Content.Client/MassMedia/Ui/NewsWriterMenu.xaml
new file mode 100644 (file)
index 0000000..64932bc
--- /dev/null
@@ -0,0 +1,45 @@
+<controls:FancyWindow
+    xmlns="https://spacestation14.io"
+    xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+    xmlns:ui="clr-namespace:Content.Client.MassMedia.Ui"
+    xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+    Title="{Loc 'news-write-ui-default-title'}"
+    MinSize="348 443"
+    SetSize="348 443">
+
+    <ui:ArticleEditorPanel Name="ArticleEditorPanel" HorizontalAlignment="Left" VerticalExpand="True"
+                           MinWidth="410" MinHeight="370" Margin="0 0 0 30" Access="Public" Visible="False"/>
+
+    <BoxContainer Orientation="Vertical" VerticalExpand="True">
+        <Control VerticalExpand="True" HorizontalExpand="True" Margin="10 10 10 0">
+            <PanelContainer Name="MainPanel" HorizontalExpand="False" VerticalExpand="True">
+                <PanelContainer.PanelOverride>
+                    <graphics:StyleBoxFlat BackgroundColor="#202023" />
+                </PanelContainer.PanelOverride>
+            </PanelContainer>
+            <ScrollContainer Name="ArticleListScrollbar" HorizontalExpand="True" VerticalExpand="True" HScrollEnabled="True">
+                <BoxContainer Name="ArticlesContainer" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Margin="6 6 6 6">
+                </BoxContainer>
+            </ScrollContainer>
+        </Control>
+        <BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="12 7 12 9">
+            <BoxContainer Orientation="Horizontal">
+                <Label Name="ArticleCount" Text="{Loc news-write-ui-article-count-0}"/>
+            </BoxContainer>
+            <Control HorizontalExpand="True"/>
+            <Control>
+                <Button Name="ButtonCreate" SetHeight="26" MinWidth="83" Text="{Loc news-write-ui-create-text}"/>
+            </Control>
+        </BoxContainer>
+        <Control SetHeight="30" Margin="2 0 0 0">
+            <PanelContainer Name="FooterPanel">
+                <PanelContainer.PanelOverride>
+                    <graphics:StyleBoxFlat BorderColor="#5A5A5A" BorderThickness="0 2 0 0" />
+                </PanelContainer.PanelOverride>
+            </PanelContainer>
+            <BoxContainer Name="ContentFooter" HorizontalExpand="True" SetHeight="28">
+                <Label Text="{Loc news-write-ui-footer-text}" VerticalAlignment="Center" Margin="6 0" StyleClasses="PdaContentFooterText"/>
+            </BoxContainer>
+        </Control>
+    </BoxContainer>
+</controls:FancyWindow>
diff --git a/Content.Client/MassMedia/Ui/NewsWriterMenu.xaml.cs b/Content.Client/MassMedia/Ui/NewsWriterMenu.xaml.cs
new file mode 100644 (file)
index 0000000..e2d5793
--- /dev/null
@@ -0,0 +1,97 @@
+using Content.Client.UserInterface.Controls;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+using Content.Shared.MassMedia.Systems;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Timing;
+
+namespace Content.Client.MassMedia.Ui;
+
+[GenerateTypedNameReferences]
+public sealed partial class NewsWriterMenu : FancyWindow
+{
+    private readonly IGameTiming _gameTiming;
+
+    private TimeSpan? _nextPublish;
+
+    public event Action<int>? DeleteButtonPressed;
+
+    public NewsWriterMenu(IGameTiming gameTiming)
+    {
+        RobustXamlLoader.Load(this);
+
+        _gameTiming = gameTiming;
+        ContentsContainer.RectClipContent = false;
+
+        // Customize scrollbar width and margin. This is not possible in xaml
+        var scrollbar = ArticleListScrollbar.GetChild(1);
+        scrollbar.SetWidth = 6f;
+        scrollbar.Margin = new Thickness(0, 0, 2 , 0);
+
+        ButtonCreate.OnPressed += OnCreate;
+    }
+
+    public void UpdateUI(NewsArticle[] articles, bool publishEnabled, TimeSpan nextPublish)
+    {
+        ArticlesContainer.Children.Clear();
+        ArticleCount.Text = Loc.GetString("news-write-ui-article-count-text", ("count", articles.Length));
+
+        //Iterate backwards to have the newest article at the top
+        for (var i = articles.Length - 1; i >= 0 ; i--)
+        {
+            var article = articles[i];
+            var control = new NewsArticleCard
+            {
+                Title = article.Title,
+                Author = article.Author ?? Loc.GetString("news-read-ui-no-author"),
+                PublicationTime = article.ShareTime,
+                ArtcileNumber = i
+            };
+            control.OnDeletePressed += () => DeleteButtonPressed?.Invoke(control.ArtcileNumber);
+
+            ArticlesContainer.AddChild(control);
+        }
+
+        ButtonCreate.Disabled = !publishEnabled;
+        _nextPublish = nextPublish;
+    }
+
+    protected override void FrameUpdate(FrameEventArgs args)
+    {
+        base.FrameUpdate(args);
+        if (!_nextPublish.HasValue)
+            return;
+
+        var remainingTime = _nextPublish.Value.Subtract(_gameTiming.CurTime);
+        if (remainingTime.TotalSeconds <= 0)
+        {
+            _nextPublish = null;
+            ButtonCreate.Text = Loc.GetString("news-write-ui-create-text");
+            return;
+        }
+
+        ButtonCreate.Text = remainingTime.Seconds.ToString("D2");
+    }
+
+    protected override void Resized()
+    {
+        base.Resized();
+        var margin = ArticleEditorPanel.Margin;
+        // Bandaid for the funny 1 pixel margin differences
+        ArticleEditorPanel.Margin =  new Thickness(Width - 1, margin.Top, margin.Right, margin.Bottom);
+    }
+
+    protected override void Dispose(bool disposing)
+    {
+        base.Dispose(disposing);
+        if (!disposing)
+            return;
+
+        ButtonCreate.OnPressed -= OnCreate;
+    }
+
+    private void OnCreate(BaseButton.ButtonEventArgs buttonEventArgs)
+    {
+        ArticleEditorPanel.Visible = true;
+    }
+}
index 30f1502779ec71693efb21ae5554937808262585..6b12cfe3a712f724cae218e4356ea84cf49d516d 100644 (file)
@@ -24,7 +24,7 @@ public sealed class PaperBoundUserInterface : BoundUserInterface
         _window.OnClose += Close;
         _window.Input.OnKeyBindDown += args => // Solution while TextEdit don't have events
         {
-            if (args.Function == EngineKeyFunctions.TextSubmit)
+            if (args.Function == EngineKeyFunctions.MultilineTextSubmit)
             {
                 var text = Rope.Collapse(_window.Input.TextRope);
                 Input_OnTextEntered(text);
index f668cdaa40faf8a97a79aded4afd8dc09b4661d5..46c054c00cfa8798c1afc268709dbcfd82696439 100644 (file)
@@ -74,6 +74,7 @@ namespace Content.Client.Stylesheets
         public const string StyleClassLabelKeyText = "LabelKeyText";
         public const string StyleClassLabelSecondaryColor = "LabelSecondaryColor";
         public const string StyleClassLabelBig = "LabelBig";
+        public const string StyleClassLabelSmall = "LabelSmall";
         public const string StyleClassButtonBig = "ButtonBig";
 
         public const string StyleClassPopupMessageSmall = "PopupMessageSmall";
@@ -329,6 +330,12 @@ namespace Content.Client.Stylesheets
             chatFilterButton.SetPatchMargin(StyleBox.Margin.All, 5);
             chatFilterButton.SetPadding(StyleBox.Margin.All, 2);
 
+            var smallButtonTex = resCache.GetTexture("/Textures/Interface/Nano/button_small.svg.96dpi.png");
+            var smallButtonBase = new StyleBoxTexture
+            {
+                Texture = smallButtonTex,
+            };
+
             var textureInvertedTriangle = resCache.GetTexture("/Textures/Interface/Nano/inverted_triangle.svg.png");
 
             var lineEditTex = resCache.GetTexture("/Textures/Interface/Nano/lineedit.png");
@@ -646,6 +653,23 @@ namespace Content.Client.Stylesheets
                     .Pseudo(ContainerButton.StylePseudoClassDisabled)
                     .Prop(Control.StylePropertyModulateSelf, ButtonColorCautionDisabled),
 
+                // Colors for confirm buttons confirm states.
+                Element<ConfirmButton>()
+                    .Pseudo(ConfirmButton.ConfirmPrefix + ContainerButton.StylePseudoClassNormal)
+                    .Prop(Control.StylePropertyModulateSelf, ButtonColorCautionDefault),
+
+                Element<ConfirmButton>()
+                    .Pseudo(ConfirmButton.ConfirmPrefix + ContainerButton.StylePseudoClassHover)
+                    .Prop(Control.StylePropertyModulateSelf, ButtonColorCautionHovered),
+
+                Element<ConfirmButton>()
+                    .Pseudo(ConfirmButton.ConfirmPrefix + ContainerButton.StylePseudoClassPressed)
+                    .Prop(Control.StylePropertyModulateSelf, ButtonColorCautionPressed),
+
+                Element<ConfirmButton>()
+                    .Pseudo(ConfirmButton.ConfirmPrefix + ContainerButton.StylePseudoClassDisabled)
+                    .Prop(Control.StylePropertyModulateSelf, ButtonColorCautionDisabled),
+
                 new StyleRule(new SelectorChild(
                     new SelectorElement(typeof(Button), null, null, new[] {ContainerButton.StylePseudoClassDisabled}),
                     new SelectorElement(typeof(Label), null, null, null)),
@@ -1189,14 +1213,6 @@ namespace Content.Client.Stylesheets
                         new StyleProperty(StripeBack.StylePropertyBackground, stripeBack),
                     }),
 
-                // StyleClassLabelBig
-                new StyleRule(
-                    SelectorElement.Class(StyleClassLabelBig),
-                    new[]
-                    {
-                        new StyleProperty("font", notoSans16),
-                    }),
-
                 // StyleClassItemStatus
                 new StyleRule(SelectorElement.Class(StyleClassItemStatus), new[]
                 {
@@ -1303,10 +1319,29 @@ namespace Content.Client.Stylesheets
                     new StyleProperty(PanelContainer.StylePropertyPanel, new StyleBoxFlat { BackgroundColor = NanoGold, ContentMarginBottomOverride = 2, ContentMarginLeftOverride = 2}),
                 }),
 
+                // Labels ---
+                Element<Label>().Class(StyleClassLabelBig)
+                    .Prop(Label.StylePropertyFont, notoSans16),
+
+                Element<Label>().Class(StyleClassLabelSmall)
+                 .Prop(Label.StylePropertyFont, notoSans10),
+                // ---
+
+                // Different Background shapes ---
                 Element<PanelContainer>().Class(ClassAngleRect)
                     .Prop(PanelContainer.StylePropertyPanel, BaseAngleRect)
                     .Prop(Control.StylePropertyModulateSelf, Color.FromHex("#25252A")),
 
+                Element<PanelContainer>().Class("BackgroundOpenRight")
+                    .Prop(PanelContainer.StylePropertyPanel, BaseButtonOpenRight)
+                    .Prop(Control.StylePropertyModulateSelf, Color.FromHex("#25252A")),
+
+                Element<PanelContainer>().Class("BackgroundOpenLeft")
+                    .Prop(PanelContainer.StylePropertyPanel, BaseButtonOpenLeft)
+                    .Prop(Control.StylePropertyModulateSelf, Color.FromHex("#25252A")),
+                // ---
+
+                // Dividers
                 Element<PanelContainer>().Class(ClassLowDivider)
                     .Prop(PanelContainer.StylePropertyPanel, new StyleBoxFlat
                     {
@@ -1393,6 +1428,15 @@ namespace Content.Client.Stylesheets
                     .Prop(Control.StylePropertyModulateSelf, ButtonColorGoodHovered),
                 // ---
 
+                // Small Button ---
+                Element<Button>().Class("ButtonSmall")
+                    .Prop(ContainerButton.StylePropertyStyleBox, smallButtonBase),
+
+                Child().Parent(Element<Button>().Class("ButtonSmall"))
+                    .Child(Element<Label>())
+                    .Prop(Label.StylePropertyFont, notoSans8),
+                // ---
+
                 Element<Label>().Class("StatusFieldTitle")
                     .Prop("font-color", NanoGold),
 
@@ -1490,7 +1534,6 @@ namespace Content.Client.Stylesheets
                     {
                         BackgroundColor = FancyTreeSelectedRowColor,
                     }),
-
             }).ToList());
         }
     }
diff --git a/Content.Client/UserInterface/Controls/ConfirmButton.cs b/Content.Client/UserInterface/Controls/ConfirmButton.cs
new file mode 100644 (file)
index 0000000..81f1135
--- /dev/null
@@ -0,0 +1,142 @@
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Timing;
+
+namespace Content.Client.UserInterface.Controls;
+
+/// <summary>
+/// A Button that requires a second click to actually invoke its OnPressed action. <br/>
+/// When clicked once it will change rendering modes to be prefixed by <see cref="ConfirmPrefix"/>
+/// and displays <see cref="ConfirmationText"/> on the button instead of <see cref="Text"/>.<br/>
+/// <br/>
+/// After the first click <see cref="CooldownTime"/> needs to elapse before it can be clicked again to confirm.<br/>
+/// When the button doesn't get clicked a second time before <see cref="ResetTime"/> passes it changes back to its normal state.<br/>
+/// </summary>
+/// <remarks>
+/// Colors for the different states need to be set in the stylesheet
+/// </remarks>
+public sealed class ConfirmButton : Button
+{
+    [Dependency] private readonly IGameTiming _gameTiming = default!;
+
+    public const string ConfirmPrefix = "confirm-";
+
+    private TimeSpan? _nextReset;
+    private TimeSpan? _nextCooldown;
+    private string? _confirmationText;
+    private string? _text;
+
+    /// <summary>
+    /// Fired when the button was pressed and confirmed
+    /// </summary>
+    public new event Action<ButtonEventArgs>? OnPressed;
+
+    /// <inheritdoc cref="Button.Text"/>
+    /// <remarks>
+    /// Hides the buttons text property to be able to sanely replace the button text with
+    /// <see cref="_confirmationText"/> when asking for confirmation
+    /// </remarks>
+    public new string? Text
+    {
+        get => _text;
+        set
+        {
+            _text = value;
+            base.Text = IsConfirming ? _confirmationText : value;
+        }
+    }
+
+    /// <summary>
+    /// The text displayed on the button when waiting for a second click
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite)]
+    public string ConfirmationText
+    {
+        get => _confirmationText ?? Loc.GetString("generic-confirm");
+        set => _confirmationText = value;
+    }
+
+    /// <summary>
+    /// The time until the button reverts to normal
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite)]
+    public TimeSpan ResetTime { get; set; } = TimeSpan.FromSeconds(2);
+
+    /// <summary>
+    /// The time until the button accepts a second click. This is to prevent accidentally confirming the button
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite)]
+    public TimeSpan CooldownTime { get; set; } = TimeSpan.FromSeconds(.5);
+
+    [ViewVariables]
+    public bool IsConfirming = false;
+
+    public ConfirmButton()
+    {
+        IoCManager.InjectDependencies(this);
+
+        base.OnPressed += HandleOnPressed;
+    }
+
+    protected override void FrameUpdate(FrameEventArgs args)
+    {
+        if (IsConfirming && _gameTiming.CurTime > _nextReset)
+        {
+            IsConfirming = false;
+            base.Text = Text;
+            DrawModeChanged();
+        }
+
+        if (Disabled && _gameTiming.CurTime > _nextCooldown)
+            Disabled = false;
+    }
+
+    protected override void DrawModeChanged()
+    {
+        if (IsConfirming)
+        {
+            switch (DrawMode)
+            {
+                case DrawModeEnum.Normal:
+                    SetOnlyStylePseudoClass(ConfirmPrefix + StylePseudoClassNormal);
+                    break;
+                case DrawModeEnum.Pressed:
+                    SetOnlyStylePseudoClass(ConfirmPrefix + StylePseudoClassPressed);
+                    break;
+                case DrawModeEnum.Hover:
+                    SetOnlyStylePseudoClass(ConfirmPrefix + StylePseudoClassHover);
+                    break;
+                case DrawModeEnum.Disabled:
+                    SetOnlyStylePseudoClass(ConfirmPrefix + StylePseudoClassDisabled);
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException();
+            }
+            return;
+        }
+
+        base.DrawModeChanged();
+    }
+
+    private void HandleOnPressed(ButtonEventArgs buttonEvent)
+    {
+        //Prevent accidental confirmations from double clicking
+        if (IsConfirming && _nextCooldown > _gameTiming.CurTime)
+            return;
+
+        switch (IsConfirming)
+        {
+            case false:
+                _nextCooldown  = _gameTiming.CurTime + CooldownTime;
+                _nextReset = _gameTiming.CurTime + ResetTime;
+                Disabled = true;
+                break;
+            case true:
+                OnPressed?.Invoke(buttonEvent);
+                break;
+        }
+
+        base.Text = IsConfirming ? Text : ConfirmationText;
+
+        IsConfirming = !IsConfirming;
+    }
+}
index da153f269779e3ac6e315c8e3951af024dc27a33..542d795235c7c30367e4abda667cae211c4136a4 100644 (file)
@@ -488,11 +488,12 @@ public sealed class ChatUIController : UIController
 
         if (_state.CurrentState is GameplayStateBase)
         {
-            // can always hear local / radio / emote when in the game
+            // can always hear local / radio / emote / notifications when in the game
             FilterableChannels |= ChatChannel.Local;
             FilterableChannels |= ChatChannel.Whisper;
             FilterableChannels |= ChatChannel.Radio;
             FilterableChannels |= ChatChannel.Emotes;
+            FilterableChannels |= ChatChannel.Notifications;
 
             // Can only send local / radio / emote when attached to a non-ghost entity.
             // TODO: this logic is iffy (checking if controlling something that's NOT a ghost), is there a better way to check this?
index 4a3b9aa568eec180204cc1cceefb2780e8872cd5..df4f56cb27cee1baa454cb56d83b3f21019ca082 100644 (file)
@@ -16,6 +16,7 @@ public sealed partial class ChannelFilterPopup : Popup
         ChatChannel.Whisper,
         ChatChannel.Emotes,
         ChatChannel.Radio,
+        ChatChannel.Notifications,
         ChatChannel.LOOC,
         ChatChannel.OOC,
         ChatChannel.Dead,
index cb08e19fc22790cab167d81c88d877ade1b9795f..4a76aef911f85407d64b22110782457c71062a50 100644 (file)
@@ -199,9 +199,13 @@ public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
             return false;
 
         var installedProgram = Spawn(prototype, new EntityCoordinates(loaderUid, 0, 0));
+        if (!TryComp(installedProgram, out CartridgeComponent? cartridge))
+            return false;
+
         _containerSystem.Insert(installedProgram, container);
 
-        UpdateCartridgeInstallationStatus(installedProgram, deinstallable ? InstallationStatus.Installed : InstallationStatus.Readonly);
+        UpdateCartridgeInstallationStatus(installedProgram, deinstallable ? InstallationStatus.Installed : InstallationStatus.Readonly, cartridge);
+        cartridge.LoaderUid = loaderUid;
 
         RaiseLocalEvent(installedProgram, new CartridgeAddedEvent(loaderUid));
         UpdateUserInterfaceState(loaderUid, loader);
@@ -223,11 +227,14 @@ public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
         if (!GetInstalled(loaderUid).Contains(programUid))
             return false;
 
+        if (TryComp(programUid, out CartridgeComponent? cartridge))
+            cartridge.LoaderUid = null;
+
         if (loader.ActiveProgram == programUid)
             loader.ActiveProgram = null;
 
         loader.BackgroundPrograms.Remove(programUid);
-        EntityManager.QueueDeleteEntity(programUid);
+        QueueDel(programUid);
         UpdateUserInterfaceState(loaderUid, loader);
         return true;
     }
@@ -308,6 +315,18 @@ public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
         loader.BackgroundPrograms.Remove(cartridgeUid);
     }
 
+    public void SendNotification(EntityUid loaderUid, string header, string message, CartridgeLoaderComponent? loader = default!)
+    {
+        if (!Resolve(loaderUid, ref loader))
+            return;
+
+        if (!loader.NotificationsEnabled)
+            return;
+
+        var args = new CartridgeLoaderNotificationSentEvent(header, message);
+        RaiseLocalEvent(loaderUid, ref args);
+    }
+
     protected override void OnItemInserted(EntityUid uid, CartridgeLoaderComponent loader, EntInsertedIntoContainerMessage args)
     {
         if (args.Container.ID != InstalledContainerId && args.Container.ID != loader.CartridgeSlot.ID)
@@ -434,13 +453,10 @@ public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
         UpdateUiState(loaderUid, null, loader);
     }
 
-    private void UpdateCartridgeInstallationStatus(EntityUid cartridgeUid, InstallationStatus installationStatus, CartridgeComponent? cartridgeComponent = default!)
+    private void UpdateCartridgeInstallationStatus(EntityUid cartridgeUid, InstallationStatus installationStatus, CartridgeComponent cartridgeComponent)
     {
-        if (Resolve(cartridgeUid, ref cartridgeComponent))
-        {
-            cartridgeComponent.InstallationStatus = installationStatus;
-            Dirty(cartridgeUid, cartridgeComponent);
-        }
+        cartridgeComponent.InstallationStatus = installationStatus;
+        Dirty(cartridgeUid, cartridgeComponent);
     }
 
     private bool HasProgram(EntityUid loader, EntityUid program, CartridgeLoaderComponent component)
similarity index 51%
rename from Content.Server/CartridgeLoader/Cartridges/NewsReadCartridgeComponent.cs
rename to Content.Server/CartridgeLoader/Cartridges/NewsReaderCartridgeComponent.cs
index d4e70fa591ebb7dfb45c28058a40c7ed3f6aaa96..525b9fa245ed4638adcb88a8d2bc33368be9c5a8 100644 (file)
@@ -1,11 +1,11 @@
 namespace Content.Server.CartridgeLoader.Cartridges;
 
 [RegisterComponent]
-public sealed partial class NewsReadCartridgeComponent : Component
+public sealed partial class NewsReaderCartridgeComponent : Component
 {
     [ViewVariables(VVAccess.ReadWrite)]
-    public int ArticleNum;
+    public int ArticleNumber;
 
-    [ViewVariables(VVAccess.ReadWrite)]
+    [ViewVariables(VVAccess.ReadWrite), DataField]
     public bool NotificationOn = true;
 }
index e398773d54fa55e6ec3f2914a370d7500a47b573..8782454eacb7a3de7cf5c5d501e1be4c905d1b0c 100644 (file)
@@ -19,6 +19,7 @@
   <ItemGroup>
     <ProjectReference Include="..\Content.Packaging\Content.Packaging.csproj" />
     <ProjectReference Include="..\Content.Server.Database\Content.Server.Database.csproj" />
+    <ProjectReference Include="..\Content.Shared.Database\Content.Shared.Database.csproj" />
     <ProjectReference Include="..\RobustToolbox\Lidgren.Network\Lidgren.Network.csproj" />
     <ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
     <ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
diff --git a/Content.Server/MassMedia/Components/NewsWriteComponent.cs b/Content.Server/MassMedia/Components/NewsWriteComponent.cs
deleted file mode 100644 (file)
index c064759..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-using Robust.Shared.Audio;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
-
-namespace Content.Server.MassMedia.Components
-{
-    [RegisterComponent]
-    public sealed partial class NewsWriteComponent : Component
-    {
-        [ViewVariables(VVAccess.ReadWrite)]
-        public bool ShareAvalible = false;
-
-        [ViewVariables(VVAccess.ReadWrite), DataField("nextShare", customTypeSerializer: typeof(TimeOffsetSerializer))]
-        public TimeSpan NextShare;
-
-        [ViewVariables(VVAccess.ReadWrite), DataField("shareCooldown")]
-        public float ShareCooldown = 60f;
-
-        [DataField("noAccessSound")]
-        public SoundSpecifier NoAccessSound = new SoundPathSpecifier("/Audio/Machines/airlock_deny.ogg");
-        [DataField("confirmSound")]
-        public SoundSpecifier ConfirmSound = new SoundPathSpecifier("/Audio/Machines/scan_finish.ogg");
-    }
-}
diff --git a/Content.Server/MassMedia/Components/NewsWriterComponent.cs b/Content.Server/MassMedia/Components/NewsWriterComponent.cs
new file mode 100644 (file)
index 0000000..7005600
--- /dev/null
@@ -0,0 +1,25 @@
+using Content.Server.MassMedia.Systems;
+using Robust.Shared.Audio;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Server.MassMedia.Components;
+
+[RegisterComponent, AutoGenerateComponentPause]
+[Access(typeof(NewsSystem))]
+public sealed partial class NewsWriterComponent : Component
+{
+    [ViewVariables(VVAccess.ReadWrite), DataField]
+    public bool PublishEnabled;
+
+    [ViewVariables(VVAccess.ReadWrite), DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
+    public TimeSpan NextPublish;
+
+    [ViewVariables(VVAccess.ReadWrite), DataField]
+    public float PublishCooldown = 20f;
+
+    [DataField]
+    public SoundSpecifier NoAccessSound = new SoundPathSpecifier("/Audio/Machines/airlock_deny.ogg");
+
+    [DataField]
+    public SoundSpecifier ConfirmSound = new SoundPathSpecifier("/Audio/Machines/scan_finish.ogg");
+}
index 01dee54cabc0b3dc457e78d6eded7822144a5762..2b18b57ff8b10dd4dc9d79a3884d91cef9d1efb8 100644 (file)
@@ -3,285 +3,321 @@ using Content.Server.Administration.Logs;
 using Content.Server.CartridgeLoader;
 using Content.Server.CartridgeLoader.Cartridges;
 using Content.Server.GameTicking;
-using Content.Server.MassMedia.Components;
-using Content.Server.PDA.Ringer;
+using System.Diagnostics.CodeAnalysis;
+using Content.Server.Access.Systems;
 using Content.Server.Popups;
-using Content.Server.StationRecords.Systems;
 using Content.Shared.Access.Components;
 using Content.Shared.Access.Systems;
 using Content.Shared.CartridgeLoader;
 using Content.Shared.CartridgeLoader.Cartridges;
 using Content.Shared.Database;
-using Content.Shared.GameTicking;
 using Content.Shared.MassMedia.Components;
 using Content.Shared.MassMedia.Systems;
-using Content.Shared.PDA;
 using Robust.Server.GameObjects;
+using Content.Server.MassMedia.Components;
 using Robust.Shared.Timing;
+using Content.Server.Station.Systems;
+using Content.Shared.Popups;
+using Content.Shared.StationRecords;
 using Robust.Shared.Audio.Systems;
-using Robust.Shared.Containers;
-using Robust.Shared.Player;
 
 namespace Content.Server.MassMedia.Systems;
 
 public sealed class NewsSystem : SharedNewsSystem
 {
     [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly IAdminLogManager _adminLogger = default!;
     [Dependency] private readonly UserInterfaceSystem _ui = default!;
-    [Dependency] private readonly RingerSystem _ringer = default!;
     [Dependency] private readonly CartridgeLoaderSystem _cartridgeLoaderSystem = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
     [Dependency] private readonly PopupSystem _popup = default!;
-    [Dependency] private readonly IAdminLogManager _adminLogger = default!;
+    [Dependency] private readonly StationSystem _station = default!;
     [Dependency] private readonly GameTicker _ticker = default!;
     [Dependency] private readonly AccessReaderSystem _accessReader = default!;
-    [Dependency] private readonly StationRecordsSystem _stationRecords = default!;
-
-    // TODO remove this. Dont store data on systems
-    // Honestly NewsSystem just needs someone to rewrite it entirely.
-    private readonly List<NewsArticle> _articles = new List<NewsArticle>();
+    [Dependency] private readonly IdCardSystem _idCardSystem = default!;
 
     public override void Initialize()
     {
         base.Initialize();
 
-        SubscribeLocalEvent<NewsWriteComponent, NewsWriteShareMessage>(OnWriteUiShareMessage);
-        SubscribeLocalEvent<NewsWriteComponent, NewsWriteDeleteMessage>(OnWriteUiDeleteMessage);
-        SubscribeLocalEvent<NewsWriteComponent, NewsWriteArticlesRequestMessage>(OnRequestWriteUiMessage);
-
-        SubscribeLocalEvent<NewsReadCartridgeComponent, CartridgeUiReadyEvent>(OnReadUiReady);
-        SubscribeLocalEvent<NewsReadCartridgeComponent, CartridgeMessageEvent>(OnReadUiMessage);
-
-        SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
-    }
+        // News writer
+        SubscribeLocalEvent<NewsWriterComponent, MapInitEvent>(OnMapInit);
 
-    private void OnRoundRestart(RoundRestartCleanupEvent ev)
-    {
-        _articles.Clear();
+        // New writer bui messages
+        Subs.BuiEvents<NewsWriterComponent>(NewsWriterUiKey.Key, subs =>
+        {
+            subs.Event<NewsWriterDeleteMessage>(OnWriteUiDeleteMessage);
+            subs.Event<NewsWriterArticlesRequestMessage>(OnRequestArticlesUiMessage);
+            subs.Event<NewsWriterPublishMessage>(OnWriteUiPublishMessage);
+        });
+
+        // News reader
+        SubscribeLocalEvent<NewsReaderCartridgeComponent, NewsArticlePublishedEvent>(OnArticlePublished);
+        SubscribeLocalEvent<NewsReaderCartridgeComponent, NewsArticleDeletedEvent>(OnArticleDeleted);
+        SubscribeLocalEvent<NewsReaderCartridgeComponent, CartridgeMessageEvent>(OnReaderUiMessage);
+        SubscribeLocalEvent<NewsReaderCartridgeComponent, CartridgeUiReadyEvent>(OnReaderUiReady);
     }
 
-    public void ToggleUi(EntityUid user, EntityUid deviceEnt, NewsWriteComponent? component)
+    public override void Update(float frameTime)
     {
-        if (!Resolve(deviceEnt, ref component))
-            return;
+        base.Update(frameTime);
 
-        if (!TryComp<ActorComponent>(user, out var actor))
-            return;
+        var query = EntityQueryEnumerator<NewsWriterComponent>();
+        while (query.MoveNext(out var uid, out var comp))
+        {
+            if (comp.PublishEnabled || _timing.CurTime < comp.NextPublish)
+                continue;
 
-        _ui.TryToggleUi(deviceEnt, NewsWriteUiKey.Key, actor.PlayerSession);
+            comp.PublishEnabled = true;
+            UpdateWriterUi((uid, comp));
+        }
     }
 
-    public void OnReadUiReady(EntityUid uid, NewsReadCartridgeComponent component, CartridgeUiReadyEvent args)
-    {
-        UpdateReadUi(uid, args.Loader, component);
-    }
+    #region Writer Event Handlers
 
-    public void UpdateWriteUi(EntityUid uid, NewsWriteComponent component)
+    private void OnMapInit(Entity<NewsWriterComponent> ent, ref MapInitEvent args)
     {
-        if (!_ui.TryGetUi(uid, NewsWriteUiKey.Key, out _))
+        var station = _station.GetOwningStation(ent);
+        if (!station.HasValue)
             return;
 
-        var state = new NewsWriteBoundUserInterfaceState(_articles.ToArray(), component.ShareAvalible);
-        _ui.TrySetUiState(uid, NewsWriteUiKey.Key, state);
+        EnsureComp<StationNewsComponent>(station.Value);
     }
 
-    public void UpdateReadUi(EntityUid uid, EntityUid loaderUid, NewsReadCartridgeComponent? component)
+    private void OnWriteUiDeleteMessage(Entity<NewsWriterComponent> ent, ref NewsWriterDeleteMessage msg)
     {
-        if (!Resolve(uid, ref component))
+        if (!TryGetArticles(ent, out var articles))
             return;
 
-        NewsReadLeafArticle(component, 0);
+        if (msg.ArticleNum >= articles.Count)
+            return;
 
-        if (_articles.Any())
-            _cartridgeLoaderSystem?.UpdateCartridgeUiState(loaderUid, new NewsReadBoundUserInterfaceState(_articles[component.ArticleNum], component.ArticleNum + 1, _articles.Count, component.NotificationOn));
+        if (msg.Session.AttachedEntity is not { } actor)
+            return;
+
+        var article = articles[msg.ArticleNum];
+        if (CheckDeleteAccess(article, ent, actor))
+        {
+            _adminLogger.Add(
+                LogType.Chat, LogImpact.Medium,
+                $"{ToPrettyString(actor):actor} deleted news article {article.Title} by {article.Author}: {article.Content}"
+                );
+
+            articles.RemoveAt(msg.ArticleNum);
+            _audio.PlayPvs(ent.Comp.ConfirmSound, ent);
+        }
         else
-            _cartridgeLoaderSystem?.UpdateCartridgeUiState(loaderUid, new NewsReadEmptyBoundUserInterfaceState(component.NotificationOn));
-    }
+        {
+            _popup.PopupEntity(Loc.GetString("news-write-no-access-popup"), ent, PopupType.SmallCaution);
+            _audio.PlayPvs(ent.Comp.NoAccessSound, ent);
+        }
 
-    private void OnReadUiMessage(EntityUid uid, NewsReadCartridgeComponent component, CartridgeMessageEvent args)
-    {
-        if (args is not NewsReadUiMessageEvent message)
-            return;
+        var args = new NewsArticleDeletedEvent();
+        var query = EntityQueryEnumerator<NewsReaderCartridgeComponent>();
+        while (query.MoveNext(out var readerUid, out _))
+        {
+            RaiseLocalEvent(readerUid, ref args);
+        }
 
-        if (message.Action == NewsReadUiAction.Next)
-            NewsReadLeafArticle(component, 1);
-        if (message.Action == NewsReadUiAction.Prev)
-            NewsReadLeafArticle(component, -1);
-        if (message.Action == NewsReadUiAction.NotificationSwith)
-            component.NotificationOn = !component.NotificationOn;
+        UpdateWriterDevices();
+    }
 
-        UpdateReadUi(uid, GetEntity(args.LoaderUid), component);
+    private void OnRequestArticlesUiMessage(Entity<NewsWriterComponent> ent, ref NewsWriterArticlesRequestMessage msg)
+    {
+        UpdateWriterUi(ent);
     }
 
-    public void OnWriteUiShareMessage(EntityUid uid, NewsWriteComponent component, NewsWriteShareMessage msg)
+    private void OnWriteUiPublishMessage(Entity<NewsWriterComponent> ent, ref NewsWriterPublishMessage msg)
     {
-        // dont blindly trust input from clients.
-        if (msg.Session.AttachedEntity is not {} author)
+        if (!ent.Comp.PublishEnabled)
             return;
 
-        if (!_accessReader.FindAccessItemsInventory(author, out var items))
-            return;
+        ent.Comp.PublishEnabled = false;
+        ent.Comp.NextPublish = _timing.CurTime + TimeSpan.FromSeconds(ent.Comp.PublishCooldown);
 
-        if (!_accessReader.FindStationRecordKeys(author, out _, items))
+        if (!TryGetArticles(ent, out var articles))
             return;
 
-        string? authorName = null;
+        if (msg.Session.AttachedEntity is not { } author)
+            return;
 
-        // TODO: There is a dedicated helper for this.
-        foreach (var item in items)
-        {
-            // ID Card
-            if (TryComp(item, out IdCardComponent? id))
-            {
-                authorName = id.FullName;
-                break;
-            }
+        if (!_accessReader.FindStationRecordKeys(author, out _))
+            return;
 
-            if (TryComp(item, out PdaComponent? pda)
-                     && pda.ContainedId != null
-                     && TryComp(pda.ContainedId, out id))
-            {
-                authorName = id.FullName;
-                break;
-            }
-        }
+        string? authorName = null;
+        if (_idCardSystem.TryFindIdCard(author, out var idCard))
+            authorName = idCard.Comp.FullName;
 
-        var trimmedName = msg.Name.Trim();
-        var trimmedContent = msg.Content.Trim();
+        var title = msg.Title.Trim();
+        var content = msg.Content.Trim();
 
         var article = new NewsArticle
         {
+            Title = title.Length <= MaxTitleLength ? title : $"{title[..MaxTitleLength]}...",
+            Content = content.Length <= MaxContentLength ? content : $"{content[..MaxContentLength]}...",
             Author = authorName,
-            Name = trimmedName.Length <= MaxNameLength ? trimmedName : $"{trimmedName[..MaxNameLength]}...",
-            Content = trimmedContent.Length <= MaxArticleLength ? trimmedContent : $"{trimmedContent[..MaxArticleLength]}...",
             ShareTime = _ticker.RoundDuration()
         };
 
-        _audio.PlayPvs(component.ConfirmSound, uid);
-        _adminLogger.Add(LogType.Chat, LogImpact.Medium, $"{ToPrettyString(author):actor} created news article {article.Name} by {article.Author}: {article.Content}");
-        _articles.Add(article);
+        _audio.PlayPvs(ent.Comp.ConfirmSound, ent);
 
-        component.ShareAvalible = false;
-        component.NextShare = _timing.CurTime + TimeSpan.FromSeconds(component.ShareCooldown);
+        _adminLogger.Add(
+            LogType.Chat,
+            LogImpact.Medium,
+            $"{ToPrettyString(author):actor} created news article {article.Title} by {article.Author}: {article.Content}"
+            );
 
-        UpdateReadDevices();
-        UpdateWriteDevices();
-        TryNotify();
+        articles.Add(article);
+
+        var args = new NewsArticlePublishedEvent(article);
+        var query = EntityQueryEnumerator<NewsReaderCartridgeComponent>();
+        while (query.MoveNext(out var readerUid, out _))
+        {
+            RaiseLocalEvent(readerUid, ref args);
+        }
+
+        UpdateWriterDevices();
     }
+    #endregion
+
+    #region Reader Event Handlers
 
-    public void OnWriteUiDeleteMessage(EntityUid uid, NewsWriteComponent component, NewsWriteDeleteMessage msg)
+    private void OnArticlePublished(Entity<NewsReaderCartridgeComponent> ent, ref NewsArticlePublishedEvent args)
     {
-        if (msg.ArticleNum > _articles.Count)
+        if (Comp<CartridgeComponent>(ent).LoaderUid is not { } loaderUid)
             return;
 
-        var articleDeleter = msg.Session.AttachedEntity;
-        if (CheckDeleteAccess(_articles[msg.ArticleNum], uid, articleDeleter))
-        {
-            if (articleDeleter != null)
-                _adminLogger.Add(LogType.Chat, LogImpact.Medium, $"{ToPrettyString(articleDeleter.Value):actor} deleted news article {_articles[msg.ArticleNum].Name} by {_articles[msg.ArticleNum].Author}: {_articles[msg.ArticleNum].Content}");
-            else
-                _adminLogger.Add(LogType.Chat, LogImpact.Medium, $"{msg.Session.Name:actor} created news article {_articles[msg.ArticleNum].Name}: {_articles[msg.ArticleNum].Content}");
-            _articles.RemoveAt(msg.ArticleNum);
-            _audio.PlayPvs(component.ConfirmSound, uid);
-        }
-        else
+        UpdateReaderUi(ent, loaderUid);
+
+        if (!ent.Comp.NotificationOn)
+            return;
+
+        _cartridgeLoaderSystem.SendNotification(
+            loaderUid,
+            Loc.GetString("news-pda-notification-header"),
+            args.Article.Title);
+    }
+
+    private void OnArticleDeleted(Entity<NewsReaderCartridgeComponent> ent, ref NewsArticleDeletedEvent args)
+    {
+        if (Comp<CartridgeComponent>(ent).LoaderUid is not { } loaderUid)
+            return;
+
+        UpdateReaderUi(ent, loaderUid);
+    }
+
+    private void OnReaderUiMessage(Entity<NewsReaderCartridgeComponent> ent, ref CartridgeMessageEvent args)
+    {
+        if (args is not NewsReaderUiMessageEvent message)
+            return;
+
+        switch (message.Action)
         {
-            _popup.PopupEntity(Loc.GetString("news-write-no-access-popup"), uid);
-            _audio.PlayPvs(component.NoAccessSound, uid);
+            case NewsReaderUiAction.Next:
+                NewsReaderLeafArticle(ent, 1);
+                break;
+            case NewsReaderUiAction.Prev:
+                NewsReaderLeafArticle(ent, -1);
+                break;
+            case NewsReaderUiAction.NotificationSwitch:
+                ent.Comp.NotificationOn = !ent.Comp.NotificationOn;
+                break;
         }
 
-        UpdateReadDevices();
-        UpdateWriteDevices();
+        UpdateReaderUi(ent, GetEntity(args.LoaderUid));
     }
 
-    public void OnRequestWriteUiMessage(EntityUid uid, NewsWriteComponent component, NewsWriteArticlesRequestMessage msg)
+    private void OnReaderUiReady(Entity<NewsReaderCartridgeComponent> ent, ref CartridgeUiReadyEvent args)
     {
-        UpdateWriteUi(uid, component);
+        UpdateReaderUi(ent, args.Loader);
     }
+    #endregion
 
-    private void NewsReadLeafArticle(NewsReadCartridgeComponent component, int leafDir)
+    private bool TryGetArticles(EntityUid uid, [NotNullWhen(true)] out List<NewsArticle>? articles)
     {
-        component.ArticleNum += leafDir;
+        if (_station.GetOwningStation(uid) is not { } station ||
+            !TryComp<StationNewsComponent>(station, out var stationNews))
+        {
+            articles = null;
+            return false;
+        }
 
-        if (component.ArticleNum >= _articles.Count) component.ArticleNum = 0;
-        if (component.ArticleNum < 0) component.ArticleNum = _articles.Count - 1;
+        articles = stationNews.Articles;
+        return true;
     }
 
-    private void TryNotify()
+    private void UpdateWriterUi(Entity<NewsWriterComponent> ent)
     {
-        var query = EntityQueryEnumerator<CartridgeLoaderComponent, RingerComponent, ContainerManagerComponent>();
+        if (!_ui.TryGetUi(ent, NewsWriterUiKey.Key, out var ui))
+            return;
 
-        while (query.MoveNext(out var uid, out var comp, out var ringer, out var cont))
-        {
-            if (!_cartridgeLoaderSystem.TryGetProgram<NewsReadCartridgeComponent>(uid, out _, out var newsReadCartridgeComponent, false, comp, cont)
-                || !newsReadCartridgeComponent.NotificationOn)
-                continue;
+        if (!TryGetArticles(ent, out var articles))
+            return;
 
-            _ringer.RingerPlayRingtone(uid, ringer);
-        }
+        var state = new NewsWriterBoundUserInterfaceState(articles.ToArray(), ent.Comp.PublishEnabled, ent.Comp.NextPublish);
+        _ui.SetUiState(ui, state);
     }
 
-    private void UpdateReadDevices()
+    private void UpdateReaderUi(Entity<NewsReaderCartridgeComponent> ent, EntityUid loaderUid)
     {
-        var query = EntityQueryEnumerator<CartridgeLoaderComponent>();
+        if (!TryGetArticles(ent, out var articles))
+            return;
 
-        while (query.MoveNext(out var owner, out var comp))
+        NewsReaderLeafArticle(ent, 0);
+
+        if (articles.Count == 0)
         {
-            if (EntityManager.TryGetComponent<NewsReadCartridgeComponent>(comp.ActiveProgram, out var cartridge))
-                UpdateReadUi(comp.ActiveProgram.Value, owner, cartridge);
+            _cartridgeLoaderSystem.UpdateCartridgeUiState(loaderUid, new NewsReaderEmptyBoundUserInterfaceState(ent.Comp.NotificationOn));
+            return;
         }
+
+        var state = new NewsReaderBoundUserInterfaceState(
+            articles[ent.Comp.ArticleNumber],
+            ent.Comp.ArticleNumber + 1,
+            articles.Count,
+            ent.Comp.NotificationOn);
+
+        _cartridgeLoaderSystem.UpdateCartridgeUiState(loaderUid, state);
     }
 
-    private void UpdateWriteDevices()
+    private void NewsReaderLeafArticle(Entity<NewsReaderCartridgeComponent> ent, int leafDir)
     {
-        var query = EntityQueryEnumerator<NewsWriteComponent>();
+        if (!TryGetArticles(ent, out var articles))
+            return;
 
-        while (query.MoveNext(out var owner, out var comp))
-        {
-            UpdateWriteUi(owner, comp);
-        }
+        ent.Comp.ArticleNumber += leafDir;
+
+        if (ent.Comp.ArticleNumber >= articles.Count)
+            ent.Comp.ArticleNumber = 0;
+
+        if (ent.Comp.ArticleNumber < 0)
+            ent.Comp.ArticleNumber = articles.Count - 1;
     }
 
-    private bool CheckDeleteAccess(NewsArticle articleToDelete, EntityUid device, EntityUid? user)
+    private void UpdateWriterDevices()
     {
-        if (EntityManager.TryGetComponent<AccessReaderComponent>(device, out var accessReader) &&
-            user.HasValue &&
-            _accessReader.IsAllowed(user.Value, device, accessReader))
+        var query = EntityQueryEnumerator<NewsWriterComponent>();
+        while (query.MoveNext(out var owner, out var comp))
         {
-            return true;
+            UpdateWriterUi((owner, comp));
         }
+    }
 
-        if (articleToDelete.AuthorStationRecordKeyIds == null ||
-            !articleToDelete.AuthorStationRecordKeyIds.Any())
-        {
+    private bool CheckDeleteAccess(NewsArticle articleToDelete, EntityUid device, EntityUid user)
+    {
+        if (TryComp<AccessReaderComponent>(device, out var accessReader) &&
+            _accessReader.IsAllowed(user, device, accessReader))
             return true;
-        }
 
-        var conv = _stationRecords.Convert(articleToDelete.AuthorStationRecordKeyIds);
-        if (user.HasValue
-            && _accessReader.FindStationRecordKeys(user.Value, out var recordKeys)
-            && recordKeys.Intersect(conv).Any())
-        {
+        if (articleToDelete.AuthorStationRecordKeyIds == null || articleToDelete.AuthorStationRecordKeyIds.Count == 0)
             return true;
-        }
 
-        return false;
+        return _accessReader.FindStationRecordKeys(user, out var recordKeys)
+               && StationRecordsToNetEntities(recordKeys).Intersect(articleToDelete.AuthorStationRecordKeyIds).Any();
     }
 
-    public override void Update(float frameTime)
+    private ICollection<(NetEntity, uint)> StationRecordsToNetEntities(IEnumerable<StationRecordKey> records)
     {
-        base.Update(frameTime);
-
-        var query = EntityQueryEnumerator<NewsWriteComponent>();
-        while (query.MoveNext(out var uid, out var comp))
-        {
-            if (comp.ShareAvalible || _timing.CurTime < comp.NextShare)
-                continue;
-
-            comp.ShareAvalible = true;
-
-            UpdateWriteUi(uid, comp);
-        }
+        return records.Select(record => (GetNetEntity(record.OriginStation), record.Id)).ToList();
     }
 }
-
index 4418f248847d50af4dcd2de4951be3ec5d3c8b78..a3436071969ef504ca564fa05b0062f758478c5b 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Server.AlertLevel;
 using Content.Server.CartridgeLoader;
+using Content.Server.Chat.Managers;
 using Content.Server.DeviceNetwork.Components;
 using Content.Server.Instruments;
 using Content.Server.Light.EntitySystems;
@@ -10,10 +11,14 @@ using Content.Server.Store.Components;
 using Content.Server.Store.Systems;
 using Content.Shared.Access.Components;
 using Content.Shared.CartridgeLoader;
+using Content.Shared.Chat;
 using Content.Shared.Light.Components;
 using Content.Shared.PDA;
+using Robust.Server.Containers;
 using Robust.Server.GameObjects;
 using Robust.Shared.Containers;
+using Robust.Shared.Player;
+using Robust.Shared.Utility;
 
 namespace Content.Server.PDA
 {
@@ -24,8 +29,10 @@ namespace Content.Server.PDA
         [Dependency] private readonly RingerSystem _ringer = default!;
         [Dependency] private readonly StationSystem _station = default!;
         [Dependency] private readonly StoreSystem _store = default!;
+        [Dependency] private readonly IChatManager _chatManager = default!;
         [Dependency] private readonly UserInterfaceSystem _ui = default!;
         [Dependency] private readonly UnpoweredFlashlightSystem _unpoweredFlashlight = default!;
+        [Dependency] private readonly ContainerSystem _containerSystem = default!;
 
         public override void Initialize()
         {
@@ -41,6 +48,8 @@ namespace Content.Server.PDA
             SubscribeLocalEvent<PdaComponent, PdaShowUplinkMessage>(OnUiMessage);
             SubscribeLocalEvent<PdaComponent, PdaLockUplinkMessage>(OnUiMessage);
 
+            SubscribeLocalEvent<PdaComponent, CartridgeLoaderNotificationSentEvent>(OnNotification);
+
             SubscribeLocalEvent<StationRenamedEvent>(OnStationRenamed);
             SubscribeLocalEvent<AlertLevelChangedEvent>(OnAlertLevelChanged);
         }
@@ -106,6 +115,28 @@ namespace Content.Server.PDA
             }
         }
 
+        private void OnNotification(Entity<PdaComponent> ent, ref CartridgeLoaderNotificationSentEvent args)
+        {
+            _ringer.RingerPlayRingtone(ent.Owner);
+
+            if (!_containerSystem.TryGetContainingContainer(ent, out var container)
+                || !TryComp<ActorComponent>(container.Owner, out var actor))
+                return;
+
+            var message = FormattedMessage.EscapeText(args.Message);
+            var wrappedMessage = Loc.GetString("pda-notification-message",
+                ("header", args.Header),
+                ("message", message));
+
+            _chatManager.ChatMessageToOne(
+                ChatChannel.Notifications,
+                message,
+                wrappedMessage,
+                EntityUid.Invalid,
+                false,
+                actor.PlayerSession.Channel);
+        }
+
         /// <summary>
         /// Send new UI state to clients, call if you modify something like uplink.
         /// </summary>
index 6e703f9c944f90bd0221abaaefb7d8d81d87d40d..822be2590db56dbd163a88bd5cc23607c7b2b32a 100644 (file)
@@ -63,13 +63,16 @@ namespace Content.Server.PDA.Ringer
             UpdateRingerUserInterface(uid, ringer, true);
         }
 
-        public void RingerPlayRingtone(EntityUid uid, RingerComponent ringer)
+        public void RingerPlayRingtone(Entity<RingerComponent?> ent)
         {
-            EnsureComp<ActiveRingerComponent>(uid);
+            if (!Resolve(ent, ref ent.Comp))
+                return;
 
-            _popupSystem.PopupEntity(Loc.GetString("comp-ringer-vibration-popup"), uid, Filter.Pvs(uid, 0.05f), false, PopupType.Small);
+            EnsureComp<ActiveRingerComponent>(ent);
 
-            UpdateRingerUserInterface(uid, ringer, true);
+            _popupSystem.PopupEntity(Loc.GetString("comp-ringer-vibration-popup"), ent, Filter.Pvs(ent, 0.05f), false, PopupType.Medium);
+
+            UpdateRingerUserInterface(ent, ent.Comp, true);
         }
 
         private void UpdateRingerUserInterfaceDriver(EntityUid uid, RingerComponent ringer, RingerRequestUpdateInterfaceMessage args)
index 56debb48f4f2a6ac7abe9a55a7a5f94327c6b55e..207e0244deef4dc74ee00c3d0f87ab18d02a506a 100644 (file)
@@ -10,6 +10,9 @@ namespace Content.Shared.CartridgeLoader;
 [RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
 public sealed partial class CartridgeComponent : Component
 {
+    [DataField]
+    public EntityUid? LoaderUid;
+
     [DataField(required: true)]
     public LocId ProgramName = "default-program-name";
 
index 5ff5fdd9a371c6356b0b64ccec109b477b305eef..c9cd710c522df3872c18847d0f7e12f0f10116cf 100644 (file)
@@ -8,7 +8,7 @@ public sealed partial class CartridgeLoaderComponent : Component
 {
     public const string CartridgeSlotId = "Cartridge-Slot";
 
-    [DataField("cartridgeSlot")]
+    [DataField]
     public ItemSlot CartridgeSlot = new();
 
     /// <summary>
@@ -32,9 +32,16 @@ public sealed partial class CartridgeLoaderComponent : Component
     /// <summary>
     /// The maximum amount of programs that can be installed on the cartridge loader entity
     /// </summary>
-    [DataField("diskSpace")]
+    [DataField]
     public int DiskSpace = 5;
 
-    [DataField("uiKey", required: true)]
+    /// <summary>
+    /// Controls whether the cartridge loader will play notifications if it supports it at all
+    /// TODO: Add an option for this to the PDA
+    /// </summary>
+    [DataField]
+    public bool NotificationsEnabled = true;
+
+    [DataField(required: true)]
     public Enum UiKey = default!;
 }
diff --git a/Content.Shared/CartridgeLoader/Cartridges/NewsReadUiMessageEvent.cs b/Content.Shared/CartridgeLoader/Cartridges/NewsReadUiMessageEvent.cs
deleted file mode 100644 (file)
index 323ee17..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.CartridgeLoader.Cartridges;
-
-[Serializable, NetSerializable]
-public sealed class NewsReadUiMessageEvent : CartridgeMessageEvent
-{
-    public readonly NewsReadUiAction Action;
-
-    public NewsReadUiMessageEvent(NewsReadUiAction action)
-    {
-        Action = action;
-    }
-}
-
-[Serializable, NetSerializable]
-public enum NewsReadUiAction
-{
-    Next,
-    Prev,
-    NotificationSwith
-}
diff --git a/Content.Shared/CartridgeLoader/Cartridges/NewsReaderUiMessageEvent.cs b/Content.Shared/CartridgeLoader/Cartridges/NewsReaderUiMessageEvent.cs
new file mode 100644 (file)
index 0000000..bab10c0
--- /dev/null
@@ -0,0 +1,22 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.CartridgeLoader.Cartridges;
+
+[Serializable, NetSerializable]
+public sealed class NewsReaderUiMessageEvent : CartridgeMessageEvent
+{
+    public readonly NewsReaderUiAction Action;
+
+    public NewsReaderUiMessageEvent(NewsReaderUiAction action)
+    {
+        Action = action;
+    }
+}
+
+[Serializable, NetSerializable]
+public enum NewsReaderUiAction
+{
+    Next,
+    Prev,
+    NotificationSwitch
+}
similarity index 60%
rename from Content.Shared/CartridgeLoader/Cartridges/NewsReadUiState.cs
rename to Content.Shared/CartridgeLoader/Cartridges/NewsReaderUiState.cs
index f0a973b014dd2e6013c98b883494f7086c7ca38f..8f7345b4c26bf9fd9a1d3f7c49a554a90edfad15 100644 (file)
@@ -4,14 +4,14 @@ using Content.Shared.MassMedia.Systems;
 namespace Content.Shared.CartridgeLoader.Cartridges;
 
 [Serializable, NetSerializable]
-public sealed class NewsReadBoundUserInterfaceState : BoundUserInterfaceState
+public sealed class NewsReaderBoundUserInterfaceState : BoundUserInterfaceState
 {
     public NewsArticle Article;
     public int TargetNum;
     public int TotalNum;
     public bool NotificationOn;
 
-    public NewsReadBoundUserInterfaceState(NewsArticle article, int targetNum, int totalNum, bool notificationOn)
+    public NewsReaderBoundUserInterfaceState(NewsArticle article, int targetNum, int totalNum, bool notificationOn)
     {
         Article = article;
         TargetNum = targetNum;
@@ -21,11 +21,11 @@ public sealed class NewsReadBoundUserInterfaceState : BoundUserInterfaceState
 }
 
 [Serializable, NetSerializable]
-public sealed class NewsReadEmptyBoundUserInterfaceState : BoundUserInterfaceState
+public sealed class NewsReaderEmptyBoundUserInterfaceState : BoundUserInterfaceState
 {
     public bool NotificationOn;
 
-    public NewsReadEmptyBoundUserInterfaceState(bool notificationOn)
+    public NewsReaderEmptyBoundUserInterfaceState(bool notificationOn)
     {
         NotificationOn = notificationOn;
     }
index 859a9f37a812c8c25434ad20ac976fed62c6432e..f276274941ee0a619a29466b4913f1a7108b50b9 100644 (file)
@@ -1,6 +1,5 @@
 using Content.Shared.Containers.ItemSlots;
 using Robust.Shared.Containers;
-using Robust.Shared.Network;
 
 namespace Content.Shared.CartridgeLoader;
 
@@ -124,3 +123,11 @@ public sealed class CartridgeUiReadyEvent : EntityEventArgs
         Loader = loader;
     }
 }
+
+/// <summary>
+/// Gets sent by the cartridge loader system to the cartridge loader entity so another system
+/// can handle displaying the notification
+/// </summary>
+/// <param name="Message">The message to be displayed</param>
+[ByRefEvent]
+public record struct CartridgeLoaderNotificationSentEvent(string Header, string Message);
index f14f33666acaf188f0fe03df05de099f5980e863..e8715a6ecb04d522c2e11396d9d1468727d74c98 100644 (file)
@@ -49,40 +49,46 @@ namespace Content.Shared.Chat
         /// </summary>
         Visual = 1 << 7,
 
+        /// <summary>
+        ///     Notifications from things like the PDA.
+        ///     Receiving a PDA message will send a notification to this channel for example
+        /// </summary>
+        Notifications = 1 << 8,
+
         /// <summary>
         ///     Emotes
         /// </summary>
-        Emotes = 1 << 8,
+        Emotes = 1 << 9,
 
         /// <summary>
         ///     Deadchat
         /// </summary>
-        Dead = 1 << 9,
+        Dead = 1 << 10,
 
         /// <summary>
         ///     Misc admin messages
         /// </summary>
-        Admin = 1 << 10,
+        Admin = 1 << 11,
 
         /// <summary>
         ///     Admin alerts, messages likely of elevated importance to admins
         /// </summary>
-        AdminAlert = 1 << 11,
+        AdminAlert = 1 << 12,
 
         /// <summary>
         ///     Admin chat
         /// </summary>
-        AdminChat = 1 << 12,
+        AdminChat = 1 << 13,
 
         /// <summary>
         ///     Unspecified.
         /// </summary>
-        Unspecified = 1 << 13,
+        Unspecified = 1 << 14,
 
         /// <summary>
         ///     Channels considered to be IC.
         /// </summary>
-        IC = Local | Whisper | Radio | Dead | Emotes | Damage | Visual,
+        IC = Local | Whisper | Radio | Dead | Emotes | Damage | Visual | Notifications,
 
         AdminRelated = Admin | AdminAlert | AdminChat,
     }
diff --git a/Content.Shared/MassMedia/Components/NewsWriterBuiMessages.cs b/Content.Shared/MassMedia/Components/NewsWriterBuiMessages.cs
new file mode 100644 (file)
index 0000000..f3dda48
--- /dev/null
@@ -0,0 +1,55 @@
+using Content.Shared.MassMedia.Systems;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.MassMedia.Components;
+
+[Serializable, NetSerializable]
+public enum NewsWriterUiKey : byte
+{
+    Key
+}
+
+[Serializable, NetSerializable]
+public sealed class NewsWriterBoundUserInterfaceState : BoundUserInterfaceState
+{
+    public readonly NewsArticle[] Articles;
+    public readonly bool PublishEnabled;
+    public readonly TimeSpan NextPublish;
+
+    public NewsWriterBoundUserInterfaceState(NewsArticle[] articles, bool publishEnabled, TimeSpan nextPublish)
+    {
+        Articles = articles;
+        PublishEnabled = publishEnabled;
+        NextPublish = nextPublish;
+    }
+}
+
+[Serializable, NetSerializable]
+public sealed class NewsWriterPublishMessage : BoundUserInterfaceMessage
+{
+    public readonly string Title;
+    public readonly string Content;
+
+
+    public NewsWriterPublishMessage(string title, string content)
+    {
+        Title = title;
+        Content = content;
+    }
+}
+
+[Serializable, NetSerializable]
+public sealed class NewsWriterDeleteMessage : BoundUserInterfaceMessage
+{
+    public readonly int ArticleNum;
+
+    public NewsWriterDeleteMessage(int num)
+    {
+        ArticleNum = num;
+    }
+}
+
+[Serializable, NetSerializable]
+public sealed class NewsWriterArticlesRequestMessage : BoundUserInterfaceMessage
+{
+}
diff --git a/Content.Shared/MassMedia/Components/SharedNewsWriteComponent.cs b/Content.Shared/MassMedia/Components/SharedNewsWriteComponent.cs
deleted file mode 100644 (file)
index 503b8ee..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-using Content.Shared.MassMedia.Systems;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.MassMedia.Components;
-
-[Serializable, NetSerializable]
-public enum NewsWriteUiKey : byte
-{
-    Key
-}
-
-[Serializable, NetSerializable]
-public sealed class NewsWriteBoundUserInterfaceState : BoundUserInterfaceState
-{
-    public NewsArticle[] Articles;
-    public bool ShareAvalible;
-
-    public NewsWriteBoundUserInterfaceState(NewsArticle[] articles, bool shareAvalible)
-    {
-        Articles = articles;
-        ShareAvalible = shareAvalible;
-    }
-}
-
-[Serializable, NetSerializable]
-public sealed class NewsWriteShareMessage : BoundUserInterfaceMessage
-{
-    public readonly string Name;
-    public readonly string Content;
-    public NewsWriteShareMessage(string name, string content)
-    {
-        Name = name;
-        Content = content;
-    }
-}
-
-[Serializable, NetSerializable]
-public sealed class NewsWriteDeleteMessage : BoundUserInterfaceMessage
-{
-    public int ArticleNum;
-
-    public NewsWriteDeleteMessage(int num)
-    {
-        ArticleNum = num;
-    }
-}
-
-[Serializable, NetSerializable]
-public sealed class NewsWriteArticlesRequestMessage : BoundUserInterfaceMessage
-{
-    public NewsWriteArticlesRequestMessage()
-    {
-    }
-}
diff --git a/Content.Shared/MassMedia/Components/StationNewsComponent.cs b/Content.Shared/MassMedia/Components/StationNewsComponent.cs
new file mode 100644 (file)
index 0000000..055b332
--- /dev/null
@@ -0,0 +1,10 @@
+using Content.Shared.MassMedia.Systems;
+
+namespace Content.Shared.MassMedia.Components;
+
+[RegisterComponent]
+public sealed partial class StationNewsComponent : Component
+{
+    [DataField]
+    public List<NewsArticle> Articles = new();
+}
index 057ce9a2edadf4955fc63ed67a001e259d196e52..f59b6af3defa4de7f496cac8356b750455b2cc7d 100644 (file)
@@ -4,16 +4,31 @@ namespace Content.Shared.MassMedia.Systems;
 
 public abstract class SharedNewsSystem : EntitySystem
 {
-    public const int MaxNameLength = 25;
-    public const int MaxArticleLength = 2048;
+    public const int MaxTitleLength = 25;
+    public const int MaxContentLength = 2048;
 }
 
 [Serializable, NetSerializable]
 public struct NewsArticle
 {
-    public string Name;
+    [ViewVariables(VVAccess.ReadWrite)]
+    public string Title;
+
+    [ViewVariables(VVAccess.ReadWrite)]
     public string Content;
+
+    [ViewVariables(VVAccess.ReadWrite)]
     public string? Author;
+
+    [ViewVariables]
     public ICollection<(NetEntity, uint)>? AuthorStationRecordKeyIds;
+
+    [ViewVariables]
     public TimeSpan ShareTime;
 }
+
+[ByRefEvent]
+public record struct NewsArticlePublishedEvent(NewsArticle Article);
+
+[ByRefEvent]
+public record struct NewsArticleDeletedEvent;
index 797b029f8e1a0e6d5509f5809a9fe47dca94c82e..720f0d15ab45a14d34d8e8dd77f6d6e530d672f3 100644 (file)
@@ -26,6 +26,7 @@ hud-chatbox-channel-Whisper = Whisper
 hud-chatbox-channel-LOOC = LOOC
 hud-chatbox-channel-OOC = OOC
 hud-chatbox-channel-Radio = Radio
+hud-chatbox-channel-Notifications = Notifications
 hud-chatbox-channel-Server = Server
 hud-chatbox-channel-Visual = Actions
 hud-chatbox-channel-Damage = Damage
index 73d15024c9566dfa3ff753e894ca3671781c6d72..d9c1305e4160b16d8ac6c3173ca378b4faa6ecd2 100644 (file)
@@ -12,3 +12,5 @@ generic-invalid = invalid
 generic-hours = hours
 
 generic-playtime-title = Playtime
+
+generic-confirm = Confirm
index 3371ffeb9f7b93b0a94467671b03d6e79f2798ed..f6a32b8613f8625c07b084bcfd234554c0d6556b 100644 (file)
@@ -1,17 +1,37 @@
-news-read-ui-next-text = Next
-news-read-ui-past-text = Past
+news-read-ui-next-text = ▶
+news-read-ui-prev-text = ◀
+news-read-ui-next-tooltip = Next
+news-read-ui-prev-tooltip = Prev
 news-read-ui-default-title = Station News
 news-read-ui-not-found-text = No articles found
 news-read-ui-time-prefix-text = Publication time:
+news-reader-ui-mute-tooltip = Mute notifications
 news-read-ui-notification-off =  ̶♫̶
 news-read-ui-notification-on = ♫
 news-read-ui-no-author = Anonymous
-news-read-ui-author-prefix = Author: 
-news-write-ui-default-title = Mass-media Management
+news-read-ui-author-prefix = Author:
+news-write-ui-default-title = News Management
 news-write-ui-articles-label = Articles:
 news-write-ui-delete-text = Delete
-news-write-ui-share-text = Publish
+news-write-ui-publish-text = Publish
+news-write-ui-create-text = Create
+news-write-ui-cancel-text = Cancel
+news-write-ui-preview-text = Preview
+news-write-ui-article-count-0 = 0 Articles
+news-write-ui-article-count-text = {$count} Articles
+news-write-ui-footer-text = News#Manager™ Authoring System
+news-write-ui-new-article = New Article
 news-write-ui-article-name-label = Heading:
 news-write-ui-article-content-label = Content:
 news-write-no-access-popup = No access
+news-writer-text-length-exceeded = Text exceeds maximum length
+news-write-ui-richtext-tooltip = News articles support rich text
+    The following rich text tags are supported:
+    {"[color=Gray][bullet/]heading \\[size=1-3\\]"}
+    {"[bullet/]bold"}
+    {"[bullet/]italic"}
+    {"[bullet/]bolditalic"}
+    {"[bullet/]color"}
+    {"[bullet/]bullet[/color]"}
 
+news-pda-notification-header = New news article
index 138cdd55141ae859003b6f581382a821552dfedb..25b8f356808241a1954222657d78dbcf89ea22f8 100644 (file)
@@ -3,7 +3,7 @@
 
 # For the PDA Ringer screen
 
-comp-ringer-vibration-popup = PDA vibrates
+comp-ringer-vibration-popup = Your PDA vibrates
 
 comp-ringer-ui-menu-title = Ringtone
 
index 85a3c94c4141be0737bee6519237c65f062d4668..7f17102c5f03e7092fe1fbd632924d02a1c1269e 100644 (file)
@@ -51,3 +51,6 @@ pda-bound-user-interface-music-button-description = Play music on your PDA
 comp-pda-ui-unknown = Unknown
 
 comp-pda-ui-unassigned = Unassigned
+
+pda-notification-message = [font size=12][bold]PDA[/bold] { $header }: [/font]
+    "{ $message }"
index 56e484a274f474fac5d0ab57a50f08bcab08f488..afbfdadf9111237163850e91b57a923ccf685078 100644 (file)
 - type: entity
   parent: BaseComputerCircuitboard
   id: ComputerMassMediaCircuitboard
-  name: mass-media console board
+  name: news manager console board
   description: Write your message to the world!
   components:
     - type: Sprite
index b9b70e6aca9bfeda4f70972593113ea967aee4f6..ae454e43a2e99d7d83dea3dcfb1b079af0a56b4f 100644 (file)
@@ -18,7 +18,7 @@
 
 - type: entity
   parent: BaseItem
-  id: NewsReadCartridge
+  id: NewsReaderCartridge
   name: news cartridge
   description: A program for reading news
   components:
     sprite: Objects/Devices/cartridge.rsi
     state: cart-y
   - type: UIFragment
-    ui: !type:NewsReadUi
+    ui: !type:NewsReaderUi
   - type: Cartridge
     programName: news-read-program-name
     icon:
       sprite: Interface/Misc/program_icons.rsi
       state: news_read
-  - type: NewsReadCartridge
+  - type: NewsReaderCartridge
 
 - type: entity
   parent: BaseItem
index 8f246958191adf896d4a586935e8dc50975b7726..0056f965a5cc7dc32a9b3c70360f2a0b7c8b5540 100644 (file)
@@ -74,7 +74,7 @@
     preinstalled:
       - CrewManifestCartridge
       - NotekeeperCartridge
-      - NewsReadCartridge
+      - NewsReaderCartridge
     cartridgeSlot:
       priority: -1
       name: device-pda-slot-component-slot-name-cartridge
     preinstalled:
       - CrewManifestCartridge
       - NotekeeperCartridge
-      - NewsReadCartridge
+      - NewsReaderCartridge
       - LogProbeCartridge
 
 - type: entity
index 9f542e9f812950f4daf73fc0d3487e3a227aeced..eb474ebb99599940bcc87803e88c13de506dce03 100644 (file)
   - type: SiliconLawProvider
     laws: Crewsimov
 
+- type: entity
+  id: BaseStationNews
+  abstract: true
+  components:
+    - type: StationNews
+
 - type: entity
   id: BaseStationAllEventsEligible
   abstract: true
index e9b04d7d04518af072616ea30761cad0535590ad..ab885b03e53727b142a8bb59ed36c3af42748822 100644 (file)
@@ -10,6 +10,7 @@
   id: StandardNanotrasenStation
   parent:
     - BaseStation
+    - BaseStationNews
     - BaseStationCargo
     - BaseStationJobsSpawning
     - BaseStationRecords
index 54d64cc72153f0a9eba82c1880f1b87e6c78febe..7ef969140b504acd17a865f6089c78ac70cacf55 100644 (file)
 - type: entity
   parent: BaseComputer
   id: ComputerMassMedia
-  name: mass-media console
+  name: news manager console
   description: Write your message to the world!
   components:
   - type: Sprite
   - type: Computer
     board: ComputerMassMediaCircuitboard
   - type: DeviceNetworkRequiresPower
-  - type: NewsWrite
+  - type: NewsWriter
   - type: AccessReader
     access: [[ "Command" ]]
   - type: ActivatableUI
-    key: enum.NewsWriteUiKey.Key
+    key: enum.NewsWriterUiKey.Key
   - type: ActivatableUIRequiresVision
   - type: Transform
     anchored: true
   - type: UserInterface
     interfaces:
-      - key: enum.NewsWriteUiKey.Key
-        type: NewsWriteBoundUserInterface
+      - key: enum.NewsWriterUiKey.Key
+        type: NewsWriterBoundUserInterface
 
 - type: entity
   parent: BaseComputer
diff --git a/Resources/Textures/Interface/Nano/button_small.svg b/Resources/Textures/Interface/Nano/button_small.svg
new file mode 100644 (file)
index 0000000..0b0c3e3
--- /dev/null
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="52"
+   height="19"
+   viewBox="0 0 13.758333 5.0270833"
+   version="1.1"
+   id="svg1"
+   xml:space="preserve"
+   inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
+   sodipodi:docname="button_small.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
+     id="namedview1"
+     pagecolor="#505050"
+     bordercolor="#eeeeee"
+     borderopacity="1"
+     inkscape:showpageshadow="0"
+     inkscape:pageopacity="0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#505050"
+     inkscape:document-units="mm"
+     inkscape:zoom="11.313709"
+     inkscape:cx="27.577164"
+     inkscape:cy="11.269514"
+     inkscape:window-width="1920"
+     inkscape:window-height="1009"
+     inkscape:window-x="-8"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1" /><defs
+     id="defs1" /><g
+     inkscape:label="Ebene 1"
+     inkscape:groupmode="layer"
+     id="layer1"><image
+       width="15.321987"
+       height="6.2645793"
+       preserveAspectRatio="none"
+       style="display:none;image-rendering:optimizeSpeed"
+       xlink:href="data:image/png;base64,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&#10;"
+       id="image1"
+       x="-0.58160484"
+       y="-0.62051022" /><path
+       style="fill:#ffffff;stroke-width:0.264583;fill-opacity:1"
+       d="M 11.940356,0.02854708 13.763697,1.8332839 V 5.0208821 H 1.8727539 L 0.04012913,3.1882573 0.03532635,0.0282692 Z"
+       id="path1"
+       sodipodi:nodetypes="ccccccc"
+       inkscape:export-filename="button_small.svg.96dpi.png"
+       inkscape:export-xdpi="96"
+       inkscape:export-ydpi="96" /></g></svg>
diff --git a/Resources/Textures/Interface/Nano/button_small.svg.96dpi.png b/Resources/Textures/Interface/Nano/button_small.svg.96dpi.png
new file mode 100644 (file)
index 0000000..bded8df
Binary files /dev/null and b/Resources/Textures/Interface/Nano/button_small.svg.96dpi.png differ
index 897506623c6ce254c0995ca124da5868a45dd80f..b8cfc40c1c482d82d14faf68e65de32991d8967b 100644 (file)
@@ -372,18 +372,24 @@ binds:
   type: State
   key: Return
   canRepeat: true
-  mod1: Shift
 - function: TextNewline
   type: State
   key: NumpadEnter
   canRepeat: true
-  mod1: Shift
 - function: TextSubmit
   type: State
   key: Return
 - function: TextSubmit
   type: State
   key: NumpadEnter
+- function: MultilineTextSubmit
+  type: State
+  key: Return
+  mod1: Control
+- function: MultilineTextSubmit
+  type: State
+  key: NumpadEnter
+  mod1: Control
 - function: TextSelectAll
   type: State
   key: A