Skip to content

Commit 239b5f9

Browse files
authored
Merge pull request #990 from dreamteamprod/dev
Release 0.27.0
2 parents 45b8ec1 + d417f88 commit 239b5f9

11 files changed

Lines changed: 415 additions & 8 deletions

File tree

client/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "client",
3-
"version": "0.26.0",
3+
"version": "0.27.0",
44
"description": "DigiScript front end",
55
"author": "DreamTeamProd",
66
"private": true,

client/src/store/modules/script.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,17 @@ export default {
326326
Vue.$toast.error('Unable to edit stage direction style');
327327
}
328328
},
329+
async GET_IMPORTABLE_STAGE_DIRECTION_STYLES() {
330+
const response = await fetch(
331+
`${makeURL('/api/v1/show/script/stage_direction_styles/import')}`,
332+
{ method: 'GET' }
333+
);
334+
if (!response.ok) {
335+
log.error('Unable to fetch importable stage direction styles');
336+
throw new Error('Failed to fetch importable styles');
337+
}
338+
return response.json();
339+
},
329340
async GET_COMPILED_SCRIPTS(context) {
330341
const response = await fetch(`${makeURL('/api/v1/show/script/compiled_scripts')}`, {
331342
method: 'GET',

client/src/vue_components/show/config/script/StageDirectionStyles.vue

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,71 @@
1111
<b-button v-if="IS_SCRIPT_EDITOR" v-b-modal.new-config-modal variant="outline-success">
1212
New Style
1313
</b-button>
14+
<b-button
15+
v-if="IS_SCRIPT_EDITOR"
16+
variant="outline-info"
17+
class="ml-2"
18+
@click="openImportModal"
19+
>
20+
Import Style
21+
</b-button>
22+
<b-modal
23+
id="import-style-modal"
24+
ref="import-style-modal"
25+
title="Import Stage Direction Style"
26+
size="xl"
27+
ok-only
28+
ok-title="Close"
29+
@hidden="resetImportState"
30+
>
31+
<div v-if="isLoadingImport" class="text-center py-3">
32+
<b-spinner />
33+
</div>
34+
<div v-else-if="importStyleGroups.length === 0" class="text-muted text-center py-3">
35+
No styles available to import from other shows.
36+
</div>
37+
<div v-else>
38+
<b-card v-for="show in importStyleGroups" :key="show.id" no-body class="mb-2">
39+
<b-card-header class="section-card-header" @click="toggleImportShow(show.id)">
40+
<div class="d-flex justify-content-between align-items-center">
41+
<span>{{ show.name }}</span>
42+
<b-icon-chevron-down v-if="styleGroupExpanded[show.id]" font-scale="0.8" />
43+
<b-icon-chevron-up v-else font-scale="0.8" />
44+
</div>
45+
</b-card-header>
46+
<b-collapse :visible="styleGroupExpanded[show.id]">
47+
<b-card-body class="p-0">
48+
<b-table :items="show.styles" :fields="importColumns" small show-empty class="mb-0">
49+
<template #cell(example)="row">
50+
<i class="example-stage-direction" :style="exampleCss(row.item)">
51+
<template v-if="row.item.text_format === 'upper'">
52+
{{ exampleText | uppercase }}
53+
</template>
54+
<template v-else-if="row.item.text_format === 'lower'">
55+
{{ exampleText | lowercase }}
56+
</template>
57+
<template v-else>
58+
{{ exampleText }}
59+
</template>
60+
</i>
61+
</template>
62+
<template #cell(btn)="row">
63+
<b-button
64+
variant="outline-success"
65+
size="sm"
66+
:disabled="!!isImporting[row.item.id]"
67+
@click="importStyle(row.item)"
68+
>
69+
<b-spinner v-if="isImporting[row.item.id]" small />
70+
<span v-else>Import</span>
71+
</b-button>
72+
</template>
73+
</b-table>
74+
</b-card-body>
75+
</b-collapse>
76+
</b-card>
77+
</div>
78+
</b-modal>
1479
<b-modal
1580
id="new-config-modal"
1681
ref="new-config-modal"
@@ -292,6 +357,11 @@ export default {
292357
{ key: 'example', label: 'Example Stage Direction' },
293358
{ key: 'btn', label: '' },
294359
],
360+
importColumns: [
361+
'description',
362+
{ key: 'example', label: 'Example Stage Direction' },
363+
{ key: 'btn', label: '' },
364+
],
295365
rowsPerPage: 15,
296366
currentPage: 1,
297367
newStyleFormState: {
@@ -322,6 +392,10 @@ export default {
322392
isSubmittingNew: false,
323393
isSubmittingEdit: false,
324394
isDeleting: false,
395+
importStyleGroups: [],
396+
styleGroupExpanded: {},
397+
isLoadingImport: false,
398+
isImporting: {},
325399
};
326400
},
327401
computed: {
@@ -587,11 +661,60 @@ export default {
587661
}
588662
return style;
589663
},
664+
async openImportModal() {
665+
this.$bvModal.show('import-style-modal');
666+
this.isLoadingImport = true;
667+
try {
668+
const data = await this.GET_IMPORTABLE_STAGE_DIRECTION_STYLES();
669+
this.importStyleGroups = data.style_groups;
670+
const expanded = {};
671+
data.style_groups.forEach((group) => {
672+
expanded[group.id] = true;
673+
});
674+
this.styleGroupExpanded = expanded;
675+
} catch (error) {
676+
log.error('Error fetching importable stage direction styles:', error);
677+
this.$toast.error('Failed to load styles for import');
678+
} finally {
679+
this.isLoadingImport = false;
680+
}
681+
},
682+
resetImportState() {
683+
this.importStyleGroups = [];
684+
this.styleGroupExpanded = {};
685+
this.isLoadingImport = false;
686+
this.isImporting = {};
687+
},
688+
toggleImportShow(showId) {
689+
this.$set(this.styleGroupExpanded, showId, !this.styleGroupExpanded[showId]);
690+
},
691+
async importStyle(style) {
692+
this.$set(this.isImporting, style.id, true);
693+
try {
694+
await this.ADD_STAGE_DIRECTION_STYLE({
695+
description: style.description,
696+
bold: style.bold,
697+
italic: style.italic,
698+
underline: style.underline,
699+
textFormat: style.text_format,
700+
textColour: style.text_colour,
701+
enableBackgroundColour: style.enable_background_colour,
702+
backgroundColour: style.background_colour,
703+
});
704+
this.$toast.success(`Imported "${style.description}"`);
705+
} catch (error) {
706+
log.error('Error importing stage direction style:', error);
707+
this.$toast.error(`Failed to import "${style.description}"`);
708+
} finally {
709+
this.$set(this.isImporting, style.id, false);
710+
}
711+
},
590712
...mapActions([
591713
'GET_STAGE_DIRECTION_STYLES',
592714
'ADD_STAGE_DIRECTION_STYLE',
593715
'DELETE_STAGE_DIRECTION_STYLE',
594716
'UPDATE_STAGE_DIRECTION_STYLE',
717+
'GET_IMPORTABLE_STAGE_DIRECTION_STYLES',
595718
]),
596719
},
597720
};

docs/pages/script_config.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,30 @@ Click **New Revision** to create a new revision. You'll need to provide a descri
6464

6565
When you create a new revision, its base state is copied from the currently loaded revision. Any changes you make to the script after that point will only affect the active revision - other revisions remain unchanged, preserving the complete history of your script.
6666

67+
### Stage Direction Styles
68+
69+
The **Stage Direction Styles** tab lets you define the visual appearance of stage direction lines throughout your script. Each style controls the text formatting applied when that style is assigned to a stage direction line.
70+
71+
#### Creating a New Style
72+
73+
Click **New Style** to open the style editor. You can configure:
74+
75+
- **Description** — a name for the style (e.g. "Narrator", "Whisper")
76+
- **Default Styles** — toggle Bold, Italic, and Underline
77+
- **Default Text Format** — Default, Uppercase, or Lowercase
78+
- **Text Colour** — a colour picker for the text colour
79+
- **Background Colour** — optionally enable and choose a background highlight colour
80+
81+
A live preview of the style is shown at the top of the editor so you can see how it will appear in the script before saving.
82+
83+
#### Importing Styles from Another Show
84+
85+
Click **Import Style** to open the import modal. This shows all stage direction styles from every other show in your DigiScript instance, grouped by show name. Each style is displayed with its live preview so you can see exactly what it will look like.
86+
87+
Click **Import** next to any style to copy it into the current show. The import creates an independent copy — changes to the imported style will not affect the original show, and vice versa. The modal stays open so you can import multiple styles in a single session.
88+
89+
If no other shows have stage direction styles defined, a message is shown indicating there are no styles available to import.
90+
6791
### Script Content
6892

6993
The **Script** tab is where you edit the actual script content. When you first navigate to this tab, you'll see an empty script interface:

electron/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

electron/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "digiscript-electron",
3-
"version": "0.26.0",
3+
"version": "0.27.0",
44
"description": "DigiScript Electron Desktop Application",
55
"author": "DreamTeamProd",
66
"license": "GPL-3.0",
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""fix_duplicate_sessions_user_id_fk
2+
3+
Migration 29471f7cf7d2 added the named FK fk_sessions_user_id_user to the
4+
sessions table but did not drop the pre-existing unnamed FK on the same column.
5+
Databases upgraded through that migration therefore ended up with two FK
6+
constraints on sessions.user_id, which causes SQLAlchemy to emit a SAWarning
7+
during every subsequent migration run.
8+
9+
This migration drops the unnamed duplicate, leaving only the named constraint
10+
with ON DELETE SET NULL. The try/except mirrors the pattern in a4d42ccfb71a:
11+
databases that were created from scratch (never had the unnamed FK) would raise
12+
IndexError from drop_constraint(None), which we safely ignore.
13+
14+
Revision ID: d2e0b8414d17
15+
Revises: d3e9f0c1a2b4
16+
Create Date: 2026-05-06 19:13:55.162159
17+
18+
"""
19+
20+
from typing import Sequence, Union
21+
22+
from alembic import op
23+
24+
25+
# revision identifiers, used by Alembic.
26+
revision: str = "d2e0b8414d17"
27+
down_revision: Union[str, None] = "d3e9f0c1a2b4"
28+
branch_labels: Union[str, Sequence[str], None] = None
29+
depends_on: Union[str, Sequence[str], None] = None
30+
31+
32+
def upgrade() -> None:
33+
# The unnamed duplicate FK is invisible to SQLAlchemy reflection, so
34+
# drop_constraint(None) finds nothing. Instead, drop and recreate the
35+
# named constraint: this forces SQLite batch mode to rebuild the table
36+
# from the reflected schema (which only sees the named FK), so the
37+
# unnamed duplicate is silently excluded from the new table.
38+
with op.batch_alter_table("sessions", schema=None) as batch_op:
39+
batch_op.drop_constraint(
40+
batch_op.f("fk_sessions_user_id_user"), type_="foreignkey"
41+
)
42+
batch_op.create_foreign_key(
43+
batch_op.f("fk_sessions_user_id_user"),
44+
"user",
45+
["user_id"],
46+
["id"],
47+
ondelete="SET NULL",
48+
)
49+
50+
51+
def downgrade() -> None:
52+
# Restore the unnamed FK (pre-29471f7cf7d2 state) alongside the named one.
53+
with op.batch_alter_table("sessions", schema=None) as batch_op:
54+
batch_op.create_foreign_key(None, "user", ["user_id"], ["id"])

server/controllers/api/show/script/stage_direction_styles.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from schemas.schemas import StageDirectionStyleSchema
1717
from utils.web.base_controller import BaseAPIController
1818
from utils.web.route import ApiRoute, ApiVersion
19-
from utils.web.web_decorators import no_live_session, requires_show
19+
from utils.web.web_decorators import api_authenticated, no_live_session, requires_show
2020

2121

2222
VALID_TEXT_FORMATS = ("default", "upper", "lower")
@@ -229,3 +229,40 @@ async def delete(self):
229229
else:
230230
self.set_status(404)
231231
await self.finish({"message": ERROR_SHOW_NOT_FOUND})
232+
233+
234+
@ApiRoute("/show/script/stage_direction_styles/import", ApiVersion.V1)
235+
class StageDirectionStylesImportController(BaseAPIController):
236+
@api_authenticated
237+
@requires_show
238+
def get(self):
239+
"""
240+
Return all stage direction styles from all other shows, grouped by show.
241+
242+
:returns: JSON with a ``shows`` key containing a list of show objects,
243+
each with ``id``, ``name``, and ``styles`` fields.
244+
"""
245+
current_show_id = self.get_current_show()["id"]
246+
schema = StageDirectionStyleSchema()
247+
248+
with self.make_session() as session:
249+
rows = session.execute(
250+
select(Show, StageDirectionStyle)
251+
.join(Script, Script.show_id == Show.id)
252+
.join(StageDirectionStyle, StageDirectionStyle.script_id == Script.id)
253+
.where(Show.id != current_show_id)
254+
.order_by(Show.id)
255+
).all()
256+
257+
shows_map: dict = {}
258+
for show, style in rows:
259+
if show.id not in shows_map:
260+
shows_map[show.id] = {
261+
"id": show.id,
262+
"name": show.name,
263+
"styles": [],
264+
}
265+
shows_map[show.id]["styles"].append(schema.dump(style))
266+
267+
self.set_status(200)
268+
self.finish({"style_groups": list(shows_map.values())})

server/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ build-backend = "setuptools.build_meta"
1111

1212
[project]
1313
name = "digiscript-server"
14-
version = "0.26.0"
14+
version = "0.27.0"
1515
description = "DigiScript server - Digital script management for theatrical shows"
1616
readme = "../README.md"
1717
requires-python = ">=3.13"

0 commit comments

Comments
 (0)