Skip to content

Commit 9254f7f

Browse files
committed
Initial commit
0 parents  commit 9254f7f

File tree

13 files changed

+618
-0
lines changed

13 files changed

+618
-0
lines changed

.editorconfig

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# EditorConfig is awesome: https://EditorConfig.org
2+
3+
# top-most EditorConfig file
4+
root = true
5+
6+
[*]
7+
indent_style = tab
8+
indent_size = 4
9+
end_of_line = lf
10+
charset = utf-8
11+
trim_trailing_whitespace = true
12+
insert_final_newline = true

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Plugin model files
2+
/wakatime-roblox-studio.rbxmx
3+
/wakatime-roblox-studio.rbxm
4+
Packages/
5+
sourcemap.json

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Roblox Studio WakaTime
2+
3+
This is a plugin for automatically tracking your time spent in Roblox Studio, with support for tracking:
4+
5+
- Time spent in the code editor (with script names)
6+
- Time spent playtesting
7+
- Time spent editing in the viewport
8+
9+
![Screenshot](./assets/plugin_screenshot.png)

assets/plugin_screenshot.png

16 KB
Loading

default.project.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "roblox-studio-wakatime",
3+
"tree": {
4+
"$path": "src",
5+
"Packages": {
6+
"$path": "Packages"
7+
}
8+
}
9+
}

rokit.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# This file lists tools managed by Rokit, a toolchain manager for Roblox projects.
2+
# For more information, see https://github.com/rojo-rbx/rokit
3+
4+
# New tools can be added by running `rokit add <tool>` in a terminal.
5+
6+
[tools]
7+
rojo = "rojo-rbx/[email protected]"
8+
wally = "UpliftGames/[email protected]"

src/init.server.luau

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
local MarketplaceService = game:GetService("MarketplaceService")
2+
local RunService = game:GetService("RunService")
3+
local ScriptEditorService = game:GetService("ScriptEditorService")
4+
local Selection = game:GetService("Selection")
5+
local StudioService = game:GetService("StudioService")
6+
local UserInputService = game:GetService("UserInputService")
7+
local storageHandler = require(script.storageHandler)
8+
local uiHandler = require(script.uiHandler)
9+
local wakatime = require(script.wakatime)
10+
11+
if RunService:IsRunning() and RunService:IsClient() then
12+
return
13+
end
14+
15+
local function activityCallback(force: boolean?)
16+
wakatime.onActivityCallback(plugin, force)
17+
end
18+
19+
local function getBestProjectName()
20+
-- If there's already a saved project name we don't need to figure it out.
21+
if storageHandler.getSavedProjectName(plugin) then
22+
return
23+
end
24+
local ok, gameDetails = pcall(MarketplaceService.GetProductInfo, MarketplaceService, game.PlaceId)
25+
if ok then
26+
storageHandler.setProjectName(plugin, gameDetails.Name)
27+
else
28+
local inferredName = storageHandler.getProjectName(plugin)
29+
if inferredName then
30+
storageHandler.setProjectName(plugin, inferredName)
31+
end
32+
end
33+
end
34+
35+
do
36+
-- While the plugin runs in playtest, we can't get the proper game name, so we'll save it in the plugin settings for it to be read later on.
37+
if not RunService:IsRunning() and not storageHandler.getSavedProjectName(plugin) then
38+
getBestProjectName()
39+
end
40+
41+
uiHandler.init(plugin)
42+
43+
UserInputService.InputBegan:Connect(function(inputObject)
44+
if inputObject.UserInputType == Enum.UserInputType.MouseMovement then
45+
return
46+
end
47+
activityCallback()
48+
end)
49+
StudioService:GetPropertyChangedSignal("ActiveScript"):Connect(activityCallback)
50+
ScriptEditorService.TextDocumentDidChange:Connect(activityCallback)
51+
Selection.SelectionChanged:Connect(activityCallback)
52+
53+
-- While in playtesting, the plugin can't run clientside because we can't access HttpService. However, the server code still runs, and we can mark this playtime.
54+
if RunService:IsRunning() then
55+
-- Accurately mark the end segment of the playtest.
56+
game:BindToClose(function()
57+
activityCallback(true)
58+
end)
59+
60+
-- The possibility that someone pressed play and left the game running in the background, with no focus on the window, or falling asleep, is still there. So we are not doing an indefinite loop, and instead limiting this to 8 iterations. After 16 minutes, playtime would no longer be on autopilot.
61+
-- Of course, if the developer focuses on the server view (either in play solo or in server mode), then input events would be registered, and the activity would continue even after this loop ends.
62+
for i = 1, 8 do
63+
activityCallback()
64+
task.wait(121)
65+
end
66+
end
67+
end

src/storageHandler.luau

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
local util = require(script.Parent.util)
2+
local module = {}
3+
4+
local projectSettingKey = `WakaTimeProject_{game.GameId}`
5+
function module.setProjectName(plugin: Plugin, value: string)
6+
value = util.trimString(value)
7+
-- Setting the project name to "" is a valid state, used for marking that it should be purposefully not sent.
8+
plugin:SetSetting(projectSettingKey, value)
9+
end
10+
11+
function module.getSavedProjectName(plugin: Plugin)
12+
return plugin:GetSetting(projectSettingKey)
13+
end
14+
15+
function module.getProjectName(plugin: Plugin): string?
16+
local existingName = module.getSavedProjectName(plugin)
17+
if existingName then
18+
return existingName
19+
end
20+
21+
if game.Name == "Server" or game.Name == "Game" then
22+
if game.GameId == 0 then
23+
return nil
24+
end
25+
return `{game.GameId}`
26+
end
27+
28+
return game.Name
29+
end
30+
31+
function module.getWakaTimeKey(plugin: Plugin)
32+
return plugin:GetSetting("WakaTimeKey") :: string?
33+
end
34+
35+
function module.setWakaTimeKey(plugin: Plugin, value: string)
36+
plugin:SetSetting("WakaTimeKey", value)
37+
end
38+
39+
function module.getShouldLogPlayTime(plugin: Plugin)
40+
local value = plugin:GetSetting("WakaTimeShouldLogPlayTime")
41+
return if value == nil then true else value
42+
end
43+
44+
function module.setShouldLogPlayTime(plugin: Plugin, value: boolean)
45+
plugin:SetSetting("WakaTimeShouldLogPlayTime", value)
46+
end
47+
48+
function module.getShouldLogEditTime(plugin: Plugin)
49+
local value = plugin:GetSetting("WakaTimeShouldLogEditTime")
50+
return if value == nil then true else value
51+
end
52+
53+
function module.setShouldLogEditTime(plugin: Plugin, value: boolean)
54+
plugin:SetSetting("WakaTimeShouldLogEditTime", value)
55+
end
56+
57+
function module.getShouldLogCodingTime(plugin: Plugin)
58+
local value = plugin:GetSetting("WakaTimeShouldLogCodingTime")
59+
return if value == nil then true else value
60+
end
61+
62+
function module.setShouldLogCodingTime(plugin: Plugin, value: boolean)
63+
plugin:SetSetting("WakaTimeShouldLogCodingTime", value)
64+
end
65+
66+
return module

src/uiHandler.luau

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
local React = require(script.Parent.Packages.React)
2+
local ReactRoblox = require(script.Parent.Packages.ReactRoblox)
3+
local StudioComponents = require(script.Parent.Packages.StudioComponents)
4+
local storageHandler = require(script.Parent.storageHandler)
5+
local util = require(script.Parent.util)
6+
local e = React.createElement
7+
local module = {}
8+
9+
local function getIcon()
10+
return if settings().Studio.Theme:GetColor(Enum.StudioStyleGuideColor.MainBackground).R < 0.5
11+
then "rbxassetid://121239006179116"
12+
else "rbxassetid://70875562655977",
13+
"Settings"
14+
end
15+
16+
local function HeaderText(props: {
17+
Text: string,
18+
})
19+
return e(StudioComponents.Label, {
20+
Text = `<b>{props.Text}</b>`,
21+
RichText = true,
22+
TextXAlignment = Enum.TextXAlignment.Left,
23+
Size = UDim2.new(1, 0, 0, 20),
24+
TextColorStyle = Enum.StudioStyleGuideColor.BrightText,
25+
})
26+
end
27+
28+
local function Checkbox(props: {
29+
Label: string,
30+
InitialValue: boolean,
31+
SetValue: (value: boolean) -> (),
32+
})
33+
local state, setState = React.useState(props.InitialValue)
34+
35+
return e(StudioComponents.Checkbox, {
36+
Label = props.Label,
37+
Value = state,
38+
OnChanged = function()
39+
local newValue = not state
40+
setState(newValue)
41+
props.SetValue(newValue)
42+
end,
43+
})
44+
end
45+
46+
local function VerticalSpace(props: { Height: number })
47+
return e("Frame", {
48+
Size = UDim2.new(1, 0, 0, props.Height),
49+
BackgroundTransparency = 1,
50+
BorderSizePixel = 0,
51+
})
52+
end
53+
54+
local function MainUI(props: {
55+
initialProject: string,
56+
setProject: (value: string) -> (),
57+
58+
initialApiKey: string,
59+
setApiKey: (value: string) -> (),
60+
61+
initialShouldLogPlayTime: boolean,
62+
setShouldLogPlayTime: (value: boolean) -> (),
63+
64+
initialShouldLogEditTime: boolean,
65+
setShouldLogEditTime: (value: boolean) -> (),
66+
67+
initialShouldLogCodingTime: boolean,
68+
setShouldLogCodingTime: (value: boolean) -> (),
69+
})
70+
local projectText, setProjectText = React.useState(props.initialProject)
71+
local keyText, setKeyText = React.useState(props.initialApiKey)
72+
73+
return e(StudioComponents.ScrollFrame, {
74+
PaddingLeft = UDim.new(0, 10),
75+
PaddingRight = UDim.new(0, 10),
76+
PaddingTop = UDim.new(0, 10),
77+
PaddingBottom = UDim.new(0, 10),
78+
}, {
79+
e(HeaderText, {
80+
Text = "Experience settings",
81+
}),
82+
83+
e(StudioComponents.Label, {
84+
Text = "Project name",
85+
TextXAlignment = Enum.TextXAlignment.Left,
86+
Size = UDim2.new(1, 0, 0, 30),
87+
}),
88+
e(StudioComponents.TextInput, {
89+
Text = projectText,
90+
OnChanged = function(newText)
91+
setProjectText(newText)
92+
props.setProject(newText)
93+
end,
94+
ClearTextOnFocus = false,
95+
PlaceholderText = "Get it at wakatime.com/api-key",
96+
}),
97+
98+
VerticalSpace({
99+
Height = 15,
100+
}),
101+
e(HeaderText, {
102+
Text = "Global settings",
103+
}),
104+
105+
e(StudioComponents.Label, {
106+
Text = `WakaTime API key{if util.isValidWakaTimeApiKey(keyText)
107+
then ""
108+
else " <b>(Please paste your API key that starts with waka_)</b>"}`,
109+
TextXAlignment = Enum.TextXAlignment.Left,
110+
Size = UDim2.new(1, 0, 0, 30),
111+
RichText = true,
112+
}),
113+
e(StudioComponents.TextInput, {
114+
Text = keyText,
115+
OnChanged = function(newText)
116+
setKeyText(newText)
117+
props.setApiKey(newText)
118+
end,
119+
ClearTextOnFocus = false,
120+
PlaceholderText = "Get it at wakatime.com/api-key",
121+
}),
122+
123+
VerticalSpace({ Height = 10 }),
124+
125+
e(Checkbox, {
126+
Label = "Log time in the editor",
127+
InitialValue = props.initialShouldLogEditTime,
128+
SetValue = props.setShouldLogEditTime,
129+
}),
130+
131+
e(Checkbox, {
132+
Label = "Log time in playtesting",
133+
InitialValue = props.initialShouldLogPlayTime,
134+
SetValue = props.setShouldLogPlayTime,
135+
}),
136+
137+
e(Checkbox, {
138+
Label = "Log time in the script editor",
139+
InitialValue = props.initialShouldLogCodingTime,
140+
SetValue = props.setShouldLogCodingTime,
141+
}),
142+
})
143+
end
144+
145+
function module.init(plugin: Plugin)
146+
local settingsWidget = plugin:CreateDockWidgetPluginGui(
147+
"wakatime_widget",
148+
DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Right, false, false, 200, 300, 50, 50)
149+
)
150+
settingsWidget.Title = "WakaTime"
151+
152+
local toolbar = plugin:CreateToolbar("WakaTime")
153+
local settingsButton =
154+
toolbar:CreateButton("wakatime_main", "Open the Roblox Studio WakaTime configuration.", getIcon())
155+
settings().Studio.ThemeChanged:Connect(function()
156+
settingsButton.Icon = getIcon()
157+
end)
158+
settingsButton.Click:Connect(function()
159+
settingsWidget.Enabled = not settingsWidget.Enabled
160+
end)
161+
settingsButton:SetActive(settingsWidget.Enabled)
162+
settingsWidget:GetPropertyChangedSignal("Enabled"):Connect(function()
163+
settingsButton:SetActive(settingsWidget.Enabled)
164+
end)
165+
166+
local root = ReactRoblox.createRoot(settingsWidget)
167+
root:render(e(MainUI, {
168+
initialProject = storageHandler.getProjectName(plugin),
169+
setProject = function(value)
170+
storageHandler.setProjectName(plugin, value)
171+
end,
172+
173+
initialApiKey = storageHandler.getWakaTimeKey(plugin) or "",
174+
setApiKey = function(value)
175+
storageHandler.setWakaTimeKey(plugin, value)
176+
end,
177+
178+
initialShouldLogPlayTime = storageHandler.getShouldLogPlayTime(plugin),
179+
setShouldLogPlayTime = function(value)
180+
storageHandler.setShouldLogPlayTime(plugin, value)
181+
end,
182+
183+
initialShouldLogEditTime = storageHandler.getShouldLogEditTime(plugin),
184+
setShouldLogEditTime = function(value)
185+
storageHandler.setShouldLogEditTime(plugin, value)
186+
end,
187+
188+
initialShouldLogCodingTime = storageHandler.getShouldLogCodingTime(plugin),
189+
setShouldLogCodingTime = function(value)
190+
storageHandler.setShouldLogCodingTime(plugin, value)
191+
end,
192+
}))
193+
end
194+
195+
return module

0 commit comments

Comments
 (0)